diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index a4d1b7ce1..1d20ee74a 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -20,55 +20,36 @@ from .models import Item, Speaker from .serializers import ItemSerializer -class AgendaPDF(PDFView): - """ - Create a full agenda-PDF. - """ - required_permission = 'agenda.can_see' - filename = ugettext_lazy('Agenda') - document_title = ugettext_lazy('Agenda') - - def append_to_pdf(self, story): - tree = Item.objects.get_tree(only_agenda_items=True, include_content=True) - - def walk_tree(tree, ancestors=0): - """ - Generator that yields a two-element-tuple. The first element is an - agenda-item and the second a number for steps to the root element. - """ - for element in tree: - yield element['item'], ancestors - yield from walk_tree(element['children'], ancestors + 1) - - for item, ancestors in walk_tree(tree): - if ancestors: - space = " " * 6 * ancestors - story.append(Paragraph( - "%s%s" % (space, escape(item.get_title())), - stylesheet['Subitem'])) - else: - story.append(Paragraph(escape(item.get_title()), stylesheet['Item'])) - +# Viewsets for the REST API class ItemViewSet(ModelViewSet): """ - API endpoint to list, retrieve, create, update and destroy agenda items. + API endpoint for agenda items. + + There are the following views: list, retrieve, create, partial_update, + update, destroy, manage_speaker, speak and tree. """ queryset = Item.objects.all() serializer_class = ItemSerializer - def check_permissions(self, request): + def check_view_permissions(self): """ - Calls self.permission_denied() if the requesting user has not the - permission to see the agenda and in case of create, update or destroy - requests the permission to manage the agenda and to see organizational - items. + Returns True if the user has required permissions. """ - if (not request.user.has_perm('agenda.can_see') or - (self.action in ('create', 'update', 'destroy') and not - (request.user.has_perm('agenda.can_manage') and - request.user.has_perm('agenda.can_see_orga_items')))): - self.permission_denied(request) + if self.action in ('list', 'retrieve', 'manage_speaker', 'tree'): + result = self.request.user.has_perm('agenda.can_see') + # For manage_speaker and tree requests the rest of the check is + # done in the specific method. See below. + elif self.action in ('create', 'partial_update', 'update', 'destroy'): + result = (self.request.user.has_perm('agenda.can_see') and + self.request.user.has_perm('agenda.can_see_orga_items') and + self.request.user.has_perm('agenda.can_manage')) + elif self.action == 'speak': + result = (self.request.user.has_perm('agenda.can_see') and + self.request.user.has_perm('agenda.can_manage')) + else: + result = False + return result def check_object_permissions(self, request, obj): """ @@ -97,11 +78,11 @@ class ItemViewSet(ModelViewSet): Checks also whether the requesting user can do this. He needs at least the permissions 'agenda.can_see' (see - self.check_permission()). In case of adding himself the permission - 'agenda.can_be_speaker' is required. In case of adding someone else - the permission 'agenda.can_manage' is required. In case of removing - someone else 'agenda.can_manage' is required. In case of removing - himself no other permission is required. + self.check_view_permissions()). In case of adding himself the + permission 'agenda.can_be_speaker' is required. In case of adding + someone else the permission 'agenda.can_manage' is required. In + case of removing someone else 'agenda.can_manage' is required. In + case of removing himself no other permission is required. """ # Retrieve item. item = self.get_object() @@ -174,16 +155,7 @@ class ItemViewSet(ModelViewSet): Special view endpoint to begin and end speach of speakers. Send PUT {'speaker': } to begin speach. Omit data to begin speach of the next speaker. Send DELETE to end speach of current speaker. - - Checks also whether the requesting user can do this. He needs at - least the permissions 'agenda.can_see' (see - self.check_permission()). Also the permission 'agenda.can_manage' - is required. """ - # Check permission. - if not self.request.user.has_perm('agenda.can_manage'): - self.permission_denied(request) - # Retrieve item. item = self.get_object() @@ -234,3 +206,35 @@ class ItemViewSet(ModelViewSet): else: return Response({'detail': 'Agenda tree successfully updated.'}) return Response(Item.objects.get_tree()) + + +# Views to generate PDFs + +class AgendaPDF(PDFView): + """ + Create a full agenda-PDF. + """ + required_permission = 'agenda.can_see' + filename = ugettext_lazy('Agenda') + document_title = ugettext_lazy('Agenda') + + def append_to_pdf(self, story): + tree = Item.objects.get_tree(only_agenda_items=True, include_content=True) + + def walk_tree(tree, ancestors=0): + """ + Generator that yields a two-element-tuple. The first element is an + agenda-item and the second a number for steps to the root element. + """ + for element in tree: + yield element['item'], ancestors + yield from walk_tree(element['children'], ancestors + 1) + + for item, ancestors in walk_tree(tree): + if ancestors: + space = " " * 6 * ancestors + story.append(Paragraph( + "%s%s" % (space, escape(item.get_title())), + stylesheet['Subitem'])) + else: + story.append(Paragraph(escape(item.get_title()), stylesheet['Item'])) diff --git a/openslides/assignments/views.py b/openslides/assignments/views.py index dabbac719..4199d5a16 100644 --- a/openslides/assignments/views.py +++ b/openslides/assignments/views.py @@ -37,24 +37,37 @@ from .serializers import ( ) +# Viewsets for the REST API + class AssignmentViewSet(ModelViewSet): """ - API endpoint to list, retrieve, create, update and destroy assignments - and to manage candidatures. + API endpoint for assignments. + + There are the following views: list, retrieve, create, partial_update, + update, destroy, candidature_self, candidature_other, mark_elected and + create_poll. """ queryset = Assignment.objects.all() - def check_permissions(self, request): + def check_view_permissions(self): """ - Calls self.permission_denied() if the requesting user has not the - permission to see assignments and in case of create, update, - partial_update or destroy requests the permission to manage - assignments. + Returns True if the user has required permissions. """ - if (not request.user.has_perm('assignments.can_see') or - (self.action in ('create', 'update', 'partial_update', 'destroy') and - not request.user.has_perm('assignments.can_manage'))): - self.permission_denied(request) + if self.action in ('list', 'retrieve'): + result = self.request.user.has_perm('assignments.can_see') + elif self.action in ('create', 'partial_update', 'update', 'destroy', + 'mark_elected', 'create_poll'): + result = (self.request.user.has_perm('assignments.can_see') and + self.request.user.has_perm('assignments.can_manage')) + elif self.action == 'candidature_self': + result = (self.request.user.has_perm('assignments.can_see') and + self.request.user.has_perm('assignments.can_nominate_self')) + elif self.action == 'candidature_other': + result = (self.request.user.has_perm('assignments.can_see') and + self.request.user.has_perm('assignments.can_nominate_other')) + else: + result = False + return result def get_serializer_class(self): """ @@ -72,8 +85,6 @@ class AssignmentViewSet(ModelViewSet): View to nominate self as candidate (POST) or withdraw own candidature (DELETE). """ - if not request.user.has_perm('assignments.can_nominate_self'): - self.permission_denied(request) assignment = self.get_object() if assignment.is_elected(request.user): raise ValidationError({'detail': _('You are already elected.')}) @@ -134,8 +145,6 @@ class AssignmentViewSet(ModelViewSet): View to nominate other users (POST) or delete their candidature status (DELETE). The client has to send {'user': }. """ - if not request.user.has_perm('assignments.can_nominate_other'): - self.permission_denied(request) user = self.get_user_from_request_data(request) assignment = self.get_object() if assignment.is_elected(user): @@ -181,8 +190,6 @@ class AssignmentViewSet(ModelViewSet): View to mark other users as elected (POST) or undo this (DELETE). The client has to send {'user': }. """ - if not request.user.has_perm('assignments.can_manage'): - self.permission_denied(request) user = self.get_user_from_request_data(request) assignment = self.get_object() if request.method == 'POST': @@ -204,8 +211,6 @@ class AssignmentViewSet(ModelViewSet): """ View to create a poll. It is a POST request without any data. """ - if not request.user.has_perm('assignments.can_manage'): - self.permission_denied(request) assignment = self.get_object() if not assignment.candidates.exists(): raise ValidationError({'detail': _('Can not create poll because there are no candidates.')}) @@ -216,21 +221,23 @@ class AssignmentViewSet(ModelViewSet): class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): """ - API endpoint to update and destroy assignment polls. + API endpoint for assignment polls. + + There are the following views: update and destroy. """ queryset = AssignmentPoll.objects.all() serializer_class = AssignmentAllPollSerializer - def check_permissions(self, request): + def check_view_permissions(self): """ - Calls self.permission_denied() if the requesting user has not the - permission to see assignments and to manage assignments. + Returns True if the user has required permissions. """ - if (not request.user.has_perm('assignments.can_see') or - not request.user.has_perm('assignments.can_manage')): - self.permission_denied(request) + return (self.request.user.has_perm('assignments.can_see') and + self.request.user.has_perm('assignments.can_manage')) +# Views to generate PDFs + class AssignmentPDF(PDFView): required_permission = 'assignments.can_see' top_space = 0 diff --git a/openslides/core/views.py b/openslides/core/views.py index 65df4549d..076883f1c 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -35,6 +35,8 @@ from .serializers import ( ) +# Special Django views + class IndexView(utils_views.CSRFMixin, utils_views.View): """ The primary view for OpenSlides using AngularJS. @@ -61,28 +63,74 @@ class ProjectorView(utils_views.View): return HttpResponse(content) +class AppsJsView(utils_views.View): + """ + Returns javascript code to be called in the angular app. + + The javascript code loads all js-files defined by the installed (django) + apps and creates the angular modules for each angular app. + """ + def get(self, *args, **kwargs): + angular_modules = [] + js_files = [] + for app_config in apps.get_app_configs(): + # Add the angular app, if the module has one. + if getattr(app_config, + 'angular_{}_module'.format(kwargs.get('openslides_app')), + False): + angular_modules.append('OpenSlidesApp.{app_name}.{app}'.format( + app=kwargs.get('openslides_app'), + app_name=app_config.label)) + + # Add all js files that the module needs + try: + app_js_files = app_config.js_files + except AttributeError: + # The app needs no js-files + pass + else: + js_files += [ + '{static}{path}'.format( + static=settings.STATIC_URL, + path=path) + for path in app_js_files] + + return HttpResponse( + "angular.module('OpenSlidesApp.{app}', {angular_modules});" + "var deferres = [];" + "{js_files}.forEach(function(js_file)deferres.push($.getScript(js_file)));" + "$.when.apply(this, deferres).done(function() angular.bootstrap(document,['OpenSlidesApp.{app}']));" + .format( + app=kwargs.get('openslides_app'), + angular_modules=angular_modules, + js_files=js_files)) + + +# Viewsets for the REST API + class ProjectorViewSet(ReadOnlyModelViewSet): """ - API endpoint to list, retrieve and update the projector slide info. + API endpoint for the projector slide info. + + There are the following views: list, retrieve, activate_elements, + prune_elements, deactivate_elements and clear_elements """ queryset = Projector.objects.all() serializer_class = ProjectorSerializer - def check_permissions(self, request): + def check_view_permissions(self): """ - Calls self.permission_denied() if the requesting user has not the - permission to see the projector and in case of an update request the - permission to manage the projector. + Returns True if the user has required permissions. """ - manage_methods = ( - 'activate_elements', - 'prune_elements', - 'deactivate_elements', - 'clear_elements') - if (not request.user.has_perm('core.can_see_projector') or - (self.action in manage_methods and - not request.user.has_perm('core.can_manage_projector'))): - self.permission_denied(request) + if self.action in ('list', 'retrieve'): + result = self.request.user.has_perm('core.can_see_projector') + elif self.action in ('activate_elements', 'prune_elements', + 'deactivate_elements', 'clear_elements'): + result = (self.request.user.has_perm('core.can_see_projector') and + self.request.user.has_perm('core.can_manage_projector')) + else: + result = False + return result @detail_route(methods=['post']) def activate_elements(self, request, pk): @@ -168,38 +216,135 @@ class ProjectorViewSet(ReadOnlyModelViewSet): class CustomSlideViewSet(ModelViewSet): """ - API endpoint to list, retrieve, create, update and destroy custom slides. + API endpoint for custom slides. + + There are the following views: list, retrieve, create, partial_update, + update and destroy. """ queryset = CustomSlide.objects.all() serializer_class = CustomSlideSerializer - def check_permissions(self, request): + def check_view_permissions(self): """ - Calls self.permission_denied() if the requesting user has not the - permission to manage projector. + Returns True if the user has required permissions. """ - if not request.user.has_perm('core.can_manage_projector'): - self.permission_denied(request) + return self.request.user.has_perm('core.can_manage_projector') class TagViewSet(ModelViewSet): """ - API endpoint to list, retrieve, create, update and destroy tags. + API endpoint for tags. + + There are the following views: list, retrieve, create, partial_update, + update and destroy. """ queryset = Tag.objects.all() serializer_class = TagSerializer - def check_permissions(self, request): + def check_view_permissions(self): """ - Calls self.permission_denied() if the requesting user has not the - permission to manage tags and it is a create, update or detroy request. - Users without permissions are able to list and retrieve tags. + Returns True if the user has required permissions. """ - if (self.action in ('create', 'update', 'destroy') and - not request.user.has_perm('core.can_manage_tags')): - self.permission_denied(request) + if self.action in ('list', 'retrieve'): + # Every authenticated user can list or retrieve 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') + else: + result = False + return result +class ConfigMetadata(SimpleMetadata): + """ + Custom metadata class to add config info to responses on OPTIONS requests. + """ + def determine_metadata(self, request, view): + # Sort config variables by weight. + config_variables = sorted(config.get_config_variables().values(), key=attrgetter('weight')) + + # Build tree. + config_groups = [] + for config_variable in config_variables: + if not config_groups or config_groups[-1]['name'] != config_variable.group: + config_groups.append(OrderedDict( + name=config_variable.group, + subgroups=[])) + if not config_groups[-1]['subgroups'] or config_groups[-1]['subgroups'][-1]['name'] != config_variable.subgroup: + config_groups[-1]['subgroups'].append(OrderedDict( + name=config_variable.subgroup, + items=[])) + config_groups[-1]['subgroups'][-1]['items'].append(config_variable.data) + + # Add tree to metadata. + metadata = super().determine_metadata(request, view) + metadata['config_groups'] = config_groups + return metadata + + +class ConfigViewSet(ViewSet): + """ + API endpoint for the config. + + There are the following views: list, retrieve and update. + """ + metadata_class = ConfigMetadata + + def check_view_permissions(self): + """ + Returns True if the user has required permissions. + """ + if self.action in ('list', 'retrieve'): + # Every authenticated user can list or retrieve the config. + # Anonymous users can do so if they are enabled. + result = self.request.user.is_authenticated() or config['general_system_enable_anonymous'] + elif self.action == 'update': + result = self.request.user.has_perm('core.can_manage_config') + else: + result = False + return result + + def list(self, request): + """ + Lists all config variables. Everybody can see them. + """ + return Response([{'key': key, 'value': value} for key, value in config.items()]) + + def retrieve(self, request, *args, **kwargs): + """ + Retrieves a config variable. Everybody can see it. + """ + key = kwargs['pk'] + try: + value = config[key] + except ConfigNotFound: + raise Http404 + return Response({'key': key, 'value': value}) + + def update(self, request, *args, **kwargs): + """ + Updates a config variable. Only managers can do this. + + Example: {"value": 42} + """ + key = kwargs['pk'] + value = request.data['value'] + + # Validate and change value. + try: + config[key] = value + except ConfigNotFound: + raise Http404 + except ConfigError as e: + raise ValidationError({'detail': e}) + + # Return response. + return Response({'key': key, 'value': value}) + + +# Special API views + class UrlPatternsView(utils_views.APIView): """ Returns a dictionary with all url patterns as json. The patterns kwargs @@ -234,121 +379,3 @@ class VersionView(utils_views.APIView): 'description': get_plugin_description(plugin), 'version': get_plugin_version(plugin)}) return result - - -class ConfigMetadata(SimpleMetadata): - """ - Custom metadata class to add config info to responses on OPTIONS requests. - """ - def determine_metadata(self, request, view): - # Sort config variables by weight. - config_variables = sorted(config.get_config_variables().values(), key=attrgetter('weight')) - - # Build tree. - config_groups = [] - for config_variable in config_variables: - if not config_groups or config_groups[-1]['name'] != config_variable.group: - config_groups.append(OrderedDict( - name=config_variable.group, - subgroups=[])) - if not config_groups[-1]['subgroups'] or config_groups[-1]['subgroups'][-1]['name'] != config_variable.subgroup: - config_groups[-1]['subgroups'].append(OrderedDict( - name=config_variable.subgroup, - items=[])) - config_groups[-1]['subgroups'][-1]['items'].append(config_variable.data) - - # Add tree to metadata. - metadata = super().determine_metadata(request, view) - metadata['config_groups'] = config_groups - return metadata - - -class ConfigViewSet(ViewSet): - """ - API endpoint to list, retrieve and update the config. - """ - metadata_class = ConfigMetadata - - def list(self, request): - """ - Lists all config variables. Everybody can see them. - """ - return Response([{'key': key, 'value': value} for key, value in config.items()]) - - def retrieve(self, request, *args, **kwargs): - """ - Retrieves a config variable. Everybody can see it. - """ - key = kwargs['pk'] - try: - value = config[key] - except ConfigNotFound: - raise Http404 - return Response({'key': key, 'value': value}) - - def update(self, request, *args, **kwargs): - """ - Updates a config variable. Only managers can do this. - - Example: {"value": 42} - """ - # Check permission. - if not request.user.has_perm('core.can_manage_config'): - self.permission_denied(request) - - key = kwargs['pk'] - value = request.data['value'] - - # Validate and change value. - try: - config[key] = value - except ConfigNotFound: - raise Http404 - except ConfigError as e: - raise ValidationError({'detail': e}) - - # Return response. - return Response({'key': key, 'value': value}) - - -class AppsJsView(utils_views.View): - """ - Returns javascript code to be called in the angular app. - - The javascript code loads all js-files defined by the installed (django) - apps and creates the angular modules for each angular app. - """ - def get(self, *args, **kwargs): - angular_modules = [] - js_files = [] - for app_config in apps.get_app_configs(): - # Add the angular app, if the module has one. - if getattr(app_config, - 'angular_{}_module'.format(kwargs.get('openslides_app')), - False): - angular_modules.append('OpenSlidesApp.{app_name}.{app}'.format( - app=kwargs.get('openslides_app'), - app_name=app_config.label)) - - # Add all js files that the module needs - try: - app_js_files = app_config.js_files - except AttributeError: - # The app needs no js-files - pass - else: - js_files += [ - '{static}{path}'.format( - static=settings.STATIC_URL, - path=path) - for path in app_js_files] - - return HttpResponse( - "angular.module('OpenSlidesApp.{app}', {angular_modules});" - "var deferres = [];" - "{js_files}.forEach(function(js_file)deferres.push($.getScript(js_file)));" - "$.when.apply(this, deferres).done(function() angular.bootstrap(document,['OpenSlidesApp.{app}']));" - .format( - app=kwargs.get('openslides_app'), - angular_modules=angular_modules, - js_files=js_files)) diff --git a/openslides/mediafiles/views.py b/openslides/mediafiles/views.py index 10a6576ba..bad984d07 100644 --- a/openslides/mediafiles/views.py +++ b/openslides/mediafiles/views.py @@ -4,23 +4,33 @@ from .models import Mediafile from .serializers import MediafileSerializer +# Viewsets for the REST API + class MediafileViewSet(ModelViewSet): """ - API endpoint to list, retrieve, create, update and destroy mediafile - objects. + API endpoint for mediafile objects. + + There are the following views: list, retrieve, create, partial_update, + update and destroy. """ queryset = Mediafile.objects.all() serializer_class = MediafileSerializer - def check_permissions(self, request): + def check_view_permissions(self): """ - Calls self.permission_denied() if the requesting user has not the - permission to see mediafile objects and in case of create, update or - destroy requests the permission to manage mediafile objects. + Returns True if the user has required permissions. """ # TODO: Use mediafiles.can_upload permission to create and update some # objects but restricted concerning the uploader. - if (not request.user.has_perm('mediafiles.can_see') or - (self.action in ('create', 'update', 'destroy') and not - request.user.has_perm('mediafiles.can_manage'))): - self.permission_denied(request) + if self.action in ('list', 'retrieve'): + result = self.request.user.has_perm('mediafiles.can_see') + elif self.action in ('create', 'partial_update', 'update'): + result = (self.request.user.has_perm('mediafiles.can_see') and + self.request.user.has_perm('mediafiles.can_upload') and + self.request.user.has_perm('mediafiles.can_manage')) + elif self.action == 'destroy': + result = (self.request.user.has_perm('mediafiles.can_see') and + self.request.user.has_perm('mediafiles.can_manage')) + else: + result = False + return result diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 218ab74b6..f0483327c 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -24,40 +24,45 @@ from .serializers import ( ) +# Viewsets for the REST API + class MotionViewSet(ModelViewSet): """ - API endpoint to list, retrieve, create, update and destroy motions. + API endpoint for motions. + + There are the following views: list, retrieve, create, partial_update, + update, destroy, manage_version, support and set_state. """ queryset = Motion.objects.all() serializer_class = MotionSerializer - def check_permissions(self, request): + def check_view_permissions(self): """ - Calls self.permission_denied() if the requesting user has not the - permission to see motions and in case of destroy requests the - permission to manage motions. + Returns True if the user has required permissions. """ - if (not request.user.has_perm('motions.can_see') or - (self.action == 'destroy' and not request.user.has_perm('motions.can_manage'))): - self.permission_denied(request) + if self.action in ('list', 'retrieve', '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. + elif self.action == 'create': + result = (self.request.user.has_perm('motions.can_see') and + self.request.user.has_perm('motions.can_create') and + (not config['motions_stop_submitting'] or + self.request.user.has_perm('motions.can_manage'))) + elif self.action in ('destroy', 'manage_version', 'set_state'): + result = (self.request.user.has_perm('motions.can_see') and + self.request.user.has_perm('motions.can_manage')) + elif self.action == 'support': + result = (self.request.user.has_perm('motions.can_see') and + self.request.user.has_perm('motions.can_support')) + else: + result = False + return result def create(self, request, *args, **kwargs): """ Customized view endpoint to create a new motion. - - Checks also whether the requesting user can submit a new motion. He - needs at least the permissions 'motions.can_see' (see - self.check_permission()) and 'motions.can_create'. If the - submitting of new motions by non-staff users is stopped via config - variable 'motions_stop_submitting', the requesting user needs also - to have the permission 'motions.can_manage'. """ - # Check permissions. - if (not request.user.has_perm('motions.can_create') or - (not config['motions_stop_submitting'] and - not request.user.has_perm('motions.can_manage'))): - self.permission_denied(request) - # Check permission to send submitter and supporter data. if (not request.user.has_perm('motions.can_manage') and (request.data.getlist('submitters') or request.data.getlist('supporters'))): @@ -80,7 +85,7 @@ class MotionViewSet(ModelViewSet): Checks also whether the requesting user can update the motion. He needs at least the permissions 'motions.can_see' (see - self.check_permission()). Also the instance method + self.check_view_permissions()). Also the instance method get_allowed_actions() is evaluated. """ # Get motion. @@ -122,10 +127,6 @@ class MotionViewSet(ModelViewSet): {'version_number': } to delete a version. Deleting the active version is not allowed. Only managers can use this view. """ - # Check permission. - if not request.user.has_perm('motions.can_manage'): - self.permission_denied(request) - # Retrieve motion and version. motion = self.get_object() version_number = request.data.get('version_number') @@ -168,17 +169,7 @@ class MotionViewSet(ModelViewSet): (unsupport). Send POST to support and DELETE to unsupport. - - Checks also whether the requesting user can do this. He needs at - least the permissions 'motions.can_see' (see - self.check_permission()). Also the the permission - 'motions.can_support' is required and the instance method - get_allowed_actions() is evaluated. """ - # Check permission. - if not request.user.has_perm('motions.can_support'): - self.permission_denied(request) - # Retrieve motion and allowed actions. motion = self.get_object() allowed_actions = motion.get_allowed_actions(request.user) @@ -211,10 +202,6 @@ class MotionViewSet(ModelViewSet): Send PUT {'state': } to set and just PUT {} to reset the state. Only managers can use this view. """ - # Check permission. - if not request.user.has_perm('motions.can_manage'): - self.permission_denied(request) - # Retrieve motion and state. motion = self.get_object() state = request.data.get('state') @@ -245,6 +232,56 @@ class MotionViewSet(ModelViewSet): return Response({'detail': message}) +class CategoryViewSet(ModelViewSet): + """ + API endpoint for categories. + + There are the following views: list, retrieve, create, partial_update, + update and destroy. + """ + queryset = Category.objects.all() + serializer_class = CategorySerializer + + def check_view_permissions(self): + """ + Returns True if the user has required permissions. + """ + if self.action in ('list', 'retrieve'): + 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 + self.request.user.has_perm('motions.can_manage')) + else: + result = False + return result + + +class WorkflowViewSet(ModelViewSet): + """ + API endpoint for workflows. + + There are the following views: list, retrieve, create, partial_update, + update and destroy. + """ + queryset = Workflow.objects.all() + serializer_class = WorkflowSerializer + + def check_view_permissions(self): + """ + Returns True if the user has required permissions. + """ + if self.action in ('list', 'retrieve'): + 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 + self.request.user.has_perm('motions.can_manage')) + else: + result = False + return result + + +# Views to generate PDFs + class PollPDFView(PDFView): """ Generates a ballotpaper. @@ -349,41 +386,3 @@ class MotionPDFView(SingleObjectMixin, PDFView): motions_to_pdf(pdf, motions) else: motion_to_pdf(pdf, self.get_object()) - - -class CategoryViewSet(ModelViewSet): - """ - API endpoint to list, retrieve, create, update and destroy categories. - """ - queryset = Category.objects.all() - serializer_class = CategorySerializer - - def check_permissions(self, request): - """ - Calls self.permission_denied() if the requesting user has not the - permission to see motions and in case of create, update or destroy - requests the permission to manage motions. - """ - if (not request.user.has_perm('motions.can_see') or - (self.action in ('create', 'update', 'destroy') and not - request.user.has_perm('motions.can_manage'))): - self.permission_denied(request) - - -class WorkflowViewSet(ModelViewSet): - """ - API endpoint to list, retrieve, create, update and destroy workflows. - """ - queryset = Workflow.objects.all() - serializer_class = WorkflowSerializer - - def check_permissions(self, request): - """ - Calls self.permission_denied() if the requesting user has not the - permission to see motions and in case of create, update or destroy - requests the permission to manage motions. - """ - if (not request.user.has_perm('motions.can_see') or - (self.action in ('create', 'update', 'destroy') and not - request.user.has_perm('motions.can_manage'))): - self.permission_denied(request) diff --git a/openslides/users/views.py b/openslides/users/views.py index c557d46fd..3bdd78a0a 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -18,6 +18,149 @@ from .serializers import ( ) +# Viewsets for the REST API + +class UserViewSet(ModelViewSet): + """ + API endpoint for users. + + There are the following views: list, retrieve, create, partial_update, + update, destroy and reset_password. + """ + queryset = User.objects.all() + + def check_view_permissions(self): + """ + Returns True if the user has required permissions. + """ + if self.action in ('list', 'retrieve'): + result = self.request.user.has_perm('users.can_see_name') + elif self.action in ('create', 'partial_update', 'update', 'destroy', 'reset_password'): + result = (self.request.user.has_perm('users.can_see_name') and + self.request.user.has_perm('users.can_see_extra_data') and + self.request.user.has_perm('users.can_manage')) + else: + result = False + return result + + def get_serializer_class(self): + """ + Returns different serializer classes with respect to action and user's + permissions. + """ + 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. + serializer_class = UserFullSerializer + else: + serializer_class = UserShortSerializer + return serializer_class + + @detail_route(methods=['post']) + def reset_password(self, request, pk=None): + """ + View to reset the password (using the default password). + """ + user = self.get_object() + user.set_password(user.default_password) + user.save() + return Response({'detail': _('Password successfully reset.')}) + + +class GroupViewSet(ModelViewSet): + """ + API endpoint for groups. + + There are the following views: list, retrieve, create, partial_update, + update and destroy. + """ + queryset = Group.objects.all() + serializer_class = GroupSerializer + + def check_view_permissions(self): + """ + Returns True if the user has required permissions. + """ + if self.action in ('list', 'retrieve'): + # Every authenticated user can list or retrieve groups. + # 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', 'partial_update', 'update', 'destroy'): + # Users with all app permissions can edit groups. + result = (self.request.user.has_perm('users.can_see_name') and + self.request.user.has_perm('users.can_see_extra_data') and + self.request.user.has_perm('users.can_manage')) + else: + # Deny request in any other case. + result = False + return result + + def destroy(self, request, *args, **kwargs): + """ + Protects builtin groups 'Anonymous' (pk=1) and 'Registered' (pk=2) + from being deleted. + """ + instance = self.get_object() + if instance.pk in (1, 2): + self.permission_denied(request) + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + + +# Special API views + +class UserLoginView(APIView): + """ + Login the user via Ajax. + """ + http_method_names = ['post'] + + def post(self, *args, **kwargs): + form = AuthenticationForm(self.request, data=self.request.data) + if form.is_valid(): + self.user = form.get_user() + auth_login(self.request, self.user) + self.success = True + else: + self.success = False + return super().post(*args, **kwargs) + + def get_context_data(self, **context): + context['success'] = self.success + if self.success: + context['user_id'] = self.user.pk + return super().get_context_data(**context) + + +class UserLogoutView(APIView): + """ + Logout the user via Ajax. + """ + http_method_names = ['post'] + + def post(self, *args, **kwargs): + auth_logout(self.request) + return super().post(*args, **kwargs) + + +class WhoAmIView(APIView): + """ + Returns the id of the requesting user. + """ + http_method_names = ['get'] + + def get_context_data(self, **context): + """ + Appends the user id into the context. + + Uses None for the anonymous user. + """ + return super().get_context_data( + user_id=self.request.user.pk, + **context) + + # Views to generate PDFs class UsersListPDF(PDFView): @@ -51,146 +194,3 @@ class UsersPasswordsPDF(PDFView): Append PDF objects. """ users_passwords_to_pdf(pdf) - - -# Viewsets for the rest api - -class UserViewSet(ModelViewSet): - """ - API endpoint to list, retrieve, create, update and delete users. - """ - queryset = User.objects.all() - - def check_permissions(self, request): - """ - Calls self.permission_denied() if the requesting user has not the - permission to see users and in case of create, update or destroy - requests the permission to see extra user data and to manage users. - """ - if (not request.user.has_perm('users.can_see_name') or - (self.action in ('create', 'update', 'destroy') and not - (request.user.has_perm('users.can_manage') and - request.user.has_perm('users.can_see_extra_data')))): - self.permission_denied(request) - - def get_serializer_class(self): - """ - Returns different serializer classes with respect to action and user's - permissions. - """ - if (self.action in ('create', 'update') or - self.request.user.has_perm('users.can_see_extra_data')): - serializer_class = UserFullSerializer - else: - serializer_class = UserShortSerializer - return serializer_class - - @detail_route(methods=['post']) - def reset_password(self, request, pk=None): - """ - View to reset the password (using the default password). - """ - if not request.user.has_perm('users.can_manage'): - self.permission_denied(request) - user = self.get_object() - user.set_password(user.default_password) - user.save() - return Response({'detail': _('Password successfully reset.')}) - - -class GroupViewSet(ModelViewSet): - """ - API endpoint to list, retrieve, create, update and delete groups. - """ - queryset = Group.objects.all() - serializer_class = GroupSerializer - - def check_permissions(self, request): - """ - Calls self.permission_denied() if the requesting user has not the - permission to see users and in case of create, update or destroy - requests the permission to see extra user data and to manage users. - """ - # Any logged in user can retrive groups. - # Anonymous user can retrive groups when they are activated. - if (self.action in ('retrieve', 'list') and - (config['general_system_enable_anonymous'] or - self.request.user.is_authenticated())): - return - - # Users with the permissions 'can_manage' and 'can_see_extra_data' can - # edit groups. - if (self.action in ('create', 'update', 'destroy', 'partial_update') and - request.user.has_perm('users.can_see_name') and - request.user.has_perm('users.can_manage') and - request.user.has_perm('users.can_see_extra_data')): - return - - # Raise permission_denied in any other case. - self.permission_denied(request) - - def destroy(self, request, *args, **kwargs): - """ - Protects builtin groups 'Anonymous' (pk=1) and 'Registered' (pk=2) - from being deleted. - """ - instance = self.get_object() - if instance.pk in (1, 2,): - self.permission_denied(request) - else: - self.perform_destroy(instance) - response = Response(status=status.HTTP_204_NO_CONTENT) - return response - - -# API Views - -class UserLoginView(APIView): - """ - Login the user via ajax. - """ - http_method_names = ['post'] - - def post(self, *args, **kwargs): - form = AuthenticationForm(self.request, data=self.request.data) - if form.is_valid(): - self.user = form.get_user() - auth_login(self.request, self.user) - self.success = True - else: - self.success = False - return super().post(*args, **kwargs) - - def get_context_data(self, **context): - context['success'] = self.success - if self.success: - context['user_id'] = self.user.pk - return super().get_context_data(**context) - - -class UserLogoutView(APIView): - """ - Logout the user via ajax. - """ - http_method_names = ['post'] - - def post(self, *args, **kwargs): - auth_logout(self.request) - return super().post(*args, **kwargs) - - -class WhoAmIView(APIView): - """ - Returns the user id in the session. - """ - http_method_names = ['get'] - - def get_context_data(self, **context): - """ - Appends the user id into the context. - - Uses None for the anonymous user. - """ - return super().get_context_data( - user_id=self.request.user.pk, - **context) diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index 5bc7f643c..19dca25ac 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -1,8 +1,7 @@ import re from urllib.parse import urlparse -from rest_framework.decorators import detail_route # noqa -from rest_framework.decorators import list_route # noqa +from rest_framework.decorators import detail_route, list_route # noqa from rest_framework.metadata import SimpleMetadata # noqa from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa from rest_framework.response import Response # noqa @@ -20,39 +19,43 @@ from rest_framework.serializers import ( # noqa SerializerMethodField, ValidationError, ) +from rest_framework.viewsets import GenericViewSet as _GenericViewSet # noqa from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa -from rest_framework.viewsets import ( # noqa - GenericViewSet, - ReadOnlyModelViewSet, - ViewSet, -) +from rest_framework.viewsets import \ + ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa +from rest_framework.viewsets import ViewSet as _ViewSet # noqa from .exceptions import OpenSlidesError router = DefaultRouter() -class ModelViewSet(_ModelViewSet): +class PermissionMixin: """ - Viewset for models. Before the method check_permission is called we - check projector requirements. If access for projector client users is - not currently required, check_permission is called, else not. + Mixin for subclasses of APIView like GenericViewSet and ModelViewSet. + + 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. """ - def initial(self, request, *args, **kwargs): - """ - Runs anything that needs to occur prior to calling the method handler. - """ - self.format_kwarg = self.get_format_suffix(**kwargs) - # Ensure that the incoming request is permitted - self.perform_authentication(request) - if not self.check_projector_requirements(): - self.check_permissions(request) - self.check_throttles(request) + 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 + permission checks by evaluating Django REST framework style permission + classes and the request passes. + """ + if not self.check_view_permissions() and not self.check_projector_requirements(): + self.permission_denied(self.request) + return () - # Perform content negotiation and store the accepted info on the request - neg = self.perform_content_negotiation(request) - request.accepted_renderer, request.accepted_media_type = neg + def check_view_permissions(self): + """ + Override this and return True if the requesting user should be able to + get access to your view. + """ + return False def check_projector_requirements(self): """ @@ -70,6 +73,22 @@ class ModelViewSet(_ModelViewSet): return result +class GenericViewSet(PermissionMixin, _GenericViewSet): + pass + + +class ModelViewSet(PermissionMixin, _ModelViewSet): + pass + + +class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet): + pass + + +class ViewSet(PermissionMixin, _ViewSet): + pass + + def get_collection_and_id_from_url(url): """ Helper function. Returns a tuple containing the collection name and the id diff --git a/openslides/utils/views.py b/openslides/utils/views.py index 090fb6826..a29ee3a84 100644 --- a/openslides/utils/views.py +++ b/openslides/utils/views.py @@ -1,8 +1,7 @@ from io import BytesIO -from django.conf import settings from django.core.exceptions import PermissionDenied -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponse from django.utils.translation import ugettext_lazy from django.views import generic as django_views from django.views.decorators.csrf import ensure_csrf_cookie @@ -16,41 +15,6 @@ from .pdf import firstPage, laterPages View = django_views.View -class PermissionMixin: - """ - Mixin for views, that only can be visited from users with special - permissions. - - Set the attribute 'required_permission' to the required permission - string or override the method 'check_permission'. - """ - required_permission = None - - def check_permission(self, request, *args, **kwargs): - """ - Checks if the user has the required permission. - """ - if self.required_permission is None: - return True - else: - return request.user.has_perm(self.required_permission) - - def dispatch(self, request, *args, **kwargs): - """ - Check if the user has the permission. - - If the user is not logged in, redirect the user to the login page. - """ - if not self.check_permission(request, *args, **kwargs): - if not request.user.is_authenticated(): - path = request.get_full_path() - return HttpResponseRedirect( - "%s?next=%s" % (settings.LOGIN_URL, path)) - else: - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - - class SingleObjectMixin(django_views.detail.SingleObjectMixin): """ Mixin for single objects from the database. @@ -88,14 +52,33 @@ class CSRFMixin: return ensure_csrf_cookie(view) -class PDFView(PermissionMixin, View): +class PDFView(View): """ View to generate an PDF. """ - filename = ugettext_lazy('undefined-filename') top_space = 3 document_title = None + required_permission = None + + def check_permission(self, request, *args, **kwargs): + """ + Checks if the user has the required permission. + """ + if self.required_permission is None: + return True + else: + return request.user.has_perm(self.required_permission) + + def dispatch(self, request, *args, **kwargs): + """ + Check if the user has the permission. + + If the user is not logged in, redirect the user to the login page. + """ + if not self.check_permission(request, *args, **kwargs): + raise PermissionDenied + return super().dispatch(request, *args, **kwargs) def get_top_space(self): return self.top_space diff --git a/tests/unit/utils/test_views.py b/tests/unit/utils/test_views.py index f58f52b64..6419d40c2 100644 --- a/tests/unit/utils/test_views.py +++ b/tests/unit/utils/test_views.py @@ -1,67 +1,9 @@ from unittest import TestCase from unittest.mock import MagicMock, patch -from django.core.exceptions import PermissionDenied - from openslides.utils import views -class PermissionMixinTest(TestCase): - def test_check_permission_non_required_permission(self): - view = views.PermissionMixin() - view.required_permission = None - request = MagicMock() - - self.assertTrue(view.check_permission(request)) - - def test_check_permission_with_required_permission(self): - view = views.PermissionMixin() - view.required_permission = 'required_permission' - request = MagicMock() - - view.check_permission(request) - - request.user.has_perm.assert_called_once_with('required_permission') - - @patch('builtins.super') - def test_dispatch_with_perm(self, mock_super): - view = views.PermissionMixin() - view.check_permission = MagicMock(return_value=True) - request = MagicMock() - - view.dispatch(request) - - mock_super().dispatch.called_once_with(request) - - @patch('openslides.utils.views.settings') - @patch('openslides.utils.views.HttpResponseRedirect') - @patch('builtins.super') - def test_dispatch_without_perm_logged_out(self, mock_super, mock_response, mock_settings): - view = views.PermissionMixin() - view.check_permission = MagicMock(return_value=False) - request = MagicMock() - request.user.is_authenticated.return_value = False - request.get_full_path.return_value = '/requested/path/' - mock_settings.LOGIN_URL = 'my_login_url' - - value = view.dispatch(request) - - mock_response.assert_called_once_with('my_login_url?next=/requested/path/') - self.assertEqual(value, mock_response()) - - @patch('openslides.utils.views.settings') - @patch('openslides.utils.views.HttpResponseRedirect') - @patch('builtins.super') - def test_dispatch_without_perm_logged_in(self, mock_super, mock_response, mock_settings): - view = views.PermissionMixin() - view.check_permission = MagicMock(return_value=False) - request = MagicMock() - request.user.is_authenticated.return_value = True - - with self.assertRaises(PermissionDenied): - view.dispatch(request) - - @patch('builtins.super') class SingleObjectMixinTest(TestCase): def test_get_object_cache(self, mock_super):