Merge pull request #1592 from normanjaeckel/CheckPermission

Refactored permission check for REST API viewsets.
This commit is contained in:
Norman Jäckel 2015-07-05 23:08:34 +02:00
commit 66f45ecd1f
9 changed files with 569 additions and 578 deletions

View File

@ -20,55 +20,36 @@ from .models import Item, Speaker
from .serializers import ItemSerializer from .serializers import ItemSerializer
class AgendaPDF(PDFView): # Viewsets for the REST API
"""
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']))
class ItemViewSet(ModelViewSet): 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() queryset = Item.objects.all()
serializer_class = ItemSerializer serializer_class = ItemSerializer
def check_permissions(self, request): def check_view_permissions(self):
""" """
Calls self.permission_denied() if the requesting user has not the Returns True if the user has required permissions.
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.
""" """
if (not request.user.has_perm('agenda.can_see') or if self.action in ('list', 'retrieve', 'manage_speaker', 'tree'):
(self.action in ('create', 'update', 'destroy') and not result = self.request.user.has_perm('agenda.can_see')
(request.user.has_perm('agenda.can_manage') and # For manage_speaker and tree requests the rest of the check is
request.user.has_perm('agenda.can_see_orga_items')))): # done in the specific method. See below.
self.permission_denied(request) 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): 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 Checks also whether the requesting user can do this. He needs at
least the permissions 'agenda.can_see' (see least the permissions 'agenda.can_see' (see
self.check_permission()). In case of adding himself the permission self.check_view_permissions()). In case of adding himself the
'agenda.can_be_speaker' is required. In case of adding someone else permission 'agenda.can_be_speaker' is required. In case of adding
the permission 'agenda.can_manage' is required. In case of removing someone else the permission 'agenda.can_manage' is required. In
someone else 'agenda.can_manage' is required. In case of removing case of removing someone else 'agenda.can_manage' is required. In
himself no other permission is required. case of removing himself no other permission is required.
""" """
# Retrieve item. # Retrieve item.
item = self.get_object() item = self.get_object()
@ -174,16 +155,7 @@ class ItemViewSet(ModelViewSet):
Special view endpoint to begin and end speach of speakers. Send PUT Special view endpoint to begin and end speach of speakers. Send PUT
{'speaker': <speaker_id>} to begin speach. Omit data to begin speach of {'speaker': <speaker_id>} to begin speach. Omit data to begin speach of
the next speaker. Send DELETE to end speach of current speaker. 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. # Retrieve item.
item = self.get_object() item = self.get_object()
@ -234,3 +206,35 @@ class ItemViewSet(ModelViewSet):
else: else:
return Response({'detail': 'Agenda tree successfully updated.'}) return Response({'detail': 'Agenda tree successfully updated.'})
return Response(Item.objects.get_tree()) 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 = "&nbsp;" * 6 * ancestors
story.append(Paragraph(
"%s%s" % (space, escape(item.get_title())),
stylesheet['Subitem']))
else:
story.append(Paragraph(escape(item.get_title()), stylesheet['Item']))

View File

@ -37,24 +37,37 @@ from .serializers import (
) )
# Viewsets for the REST API
class AssignmentViewSet(ModelViewSet): class AssignmentViewSet(ModelViewSet):
""" """
API endpoint to list, retrieve, create, update and destroy assignments API endpoint for assignments.
and to manage candidatures.
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() 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 Returns True if the user has required permissions.
permission to see assignments and in case of create, update,
partial_update or destroy requests the permission to manage
assignments.
""" """
if (not request.user.has_perm('assignments.can_see') or if self.action in ('list', 'retrieve'):
(self.action in ('create', 'update', 'partial_update', 'destroy') and result = self.request.user.has_perm('assignments.can_see')
not request.user.has_perm('assignments.can_manage'))): elif self.action in ('create', 'partial_update', 'update', 'destroy',
self.permission_denied(request) '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): def get_serializer_class(self):
""" """
@ -72,8 +85,6 @@ class AssignmentViewSet(ModelViewSet):
View to nominate self as candidate (POST) or withdraw own View to nominate self as candidate (POST) or withdraw own
candidature (DELETE). candidature (DELETE).
""" """
if not request.user.has_perm('assignments.can_nominate_self'):
self.permission_denied(request)
assignment = self.get_object() assignment = self.get_object()
if assignment.is_elected(request.user): if assignment.is_elected(request.user):
raise ValidationError({'detail': _('You are already elected.')}) raise ValidationError({'detail': _('You are already elected.')})
@ -134,8 +145,6 @@ class AssignmentViewSet(ModelViewSet):
View to nominate other users (POST) or delete their candidature View to nominate other users (POST) or delete their candidature
status (DELETE). The client has to send {'user': <id>}. status (DELETE). The client has to send {'user': <id>}.
""" """
if not request.user.has_perm('assignments.can_nominate_other'):
self.permission_denied(request)
user = self.get_user_from_request_data(request) user = self.get_user_from_request_data(request)
assignment = self.get_object() assignment = self.get_object()
if assignment.is_elected(user): if assignment.is_elected(user):
@ -181,8 +190,6 @@ class AssignmentViewSet(ModelViewSet):
View to mark other users as elected (POST) or undo this (DELETE). View to mark other users as elected (POST) or undo this (DELETE).
The client has to send {'user': <id>}. The client has to send {'user': <id>}.
""" """
if not request.user.has_perm('assignments.can_manage'):
self.permission_denied(request)
user = self.get_user_from_request_data(request) user = self.get_user_from_request_data(request)
assignment = self.get_object() assignment = self.get_object()
if request.method == 'POST': if request.method == 'POST':
@ -204,8 +211,6 @@ class AssignmentViewSet(ModelViewSet):
""" """
View to create a poll. It is a POST request without any data. 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() assignment = self.get_object()
if not assignment.candidates.exists(): if not assignment.candidates.exists():
raise ValidationError({'detail': _('Can not create poll because there are no candidates.')}) raise ValidationError({'detail': _('Can not create poll because there are no candidates.')})
@ -216,21 +221,23 @@ class AssignmentViewSet(ModelViewSet):
class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): 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() queryset = AssignmentPoll.objects.all()
serializer_class = AssignmentAllPollSerializer serializer_class = AssignmentAllPollSerializer
def check_permissions(self, request): def check_view_permissions(self):
""" """
Calls self.permission_denied() if the requesting user has not the Returns True if the user has required permissions.
permission to see assignments and to manage assignments.
""" """
if (not request.user.has_perm('assignments.can_see') or return (self.request.user.has_perm('assignments.can_see') and
not request.user.has_perm('assignments.can_manage')): self.request.user.has_perm('assignments.can_manage'))
self.permission_denied(request)
# Views to generate PDFs
class AssignmentPDF(PDFView): class AssignmentPDF(PDFView):
required_permission = 'assignments.can_see' required_permission = 'assignments.can_see'
top_space = 0 top_space = 0

View File

@ -35,6 +35,8 @@ from .serializers import (
) )
# Special Django views
class IndexView(utils_views.CSRFMixin, utils_views.View): class IndexView(utils_views.CSRFMixin, utils_views.View):
""" """
The primary view for OpenSlides using AngularJS. The primary view for OpenSlides using AngularJS.
@ -61,28 +63,74 @@ class ProjectorView(utils_views.View):
return HttpResponse(content) 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): 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() queryset = Projector.objects.all()
serializer_class = ProjectorSerializer serializer_class = ProjectorSerializer
def check_permissions(self, request): def check_view_permissions(self):
""" """
Calls self.permission_denied() if the requesting user has not the Returns True if the user has required permissions.
permission to see the projector and in case of an update request the
permission to manage the projector.
""" """
manage_methods = ( if self.action in ('list', 'retrieve'):
'activate_elements', result = self.request.user.has_perm('core.can_see_projector')
'prune_elements', elif self.action in ('activate_elements', 'prune_elements',
'deactivate_elements', 'deactivate_elements', 'clear_elements'):
'clear_elements') result = (self.request.user.has_perm('core.can_see_projector') and
if (not request.user.has_perm('core.can_see_projector') or self.request.user.has_perm('core.can_manage_projector'))
(self.action in manage_methods and else:
not request.user.has_perm('core.can_manage_projector'))): result = False
self.permission_denied(request) return result
@detail_route(methods=['post']) @detail_route(methods=['post'])
def activate_elements(self, request, pk): def activate_elements(self, request, pk):
@ -168,38 +216,135 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
class CustomSlideViewSet(ModelViewSet): 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() queryset = CustomSlide.objects.all()
serializer_class = CustomSlideSerializer serializer_class = CustomSlideSerializer
def check_permissions(self, request): def check_view_permissions(self):
""" """
Calls self.permission_denied() if the requesting user has not the Returns True if the user has required permissions.
permission to manage projector.
""" """
if not request.user.has_perm('core.can_manage_projector'): return self.request.user.has_perm('core.can_manage_projector')
self.permission_denied(request)
class TagViewSet(ModelViewSet): 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() queryset = Tag.objects.all()
serializer_class = TagSerializer serializer_class = TagSerializer
def check_permissions(self, request): def check_view_permissions(self):
""" """
Calls self.permission_denied() if the requesting user has not the Returns True if the user has required permissions.
permission to manage tags and it is a create, update or detroy request.
Users without permissions are able to list and retrieve tags.
""" """
if (self.action in ('create', 'update', 'destroy') and if self.action in ('list', 'retrieve'):
not request.user.has_perm('core.can_manage_tags')): # Every authenticated user can list or retrieve tags.
self.permission_denied(request) # 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): class UrlPatternsView(utils_views.APIView):
""" """
Returns a dictionary with all url patterns as json. The patterns kwargs 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), 'description': get_plugin_description(plugin),
'version': get_plugin_version(plugin)}) 'version': get_plugin_version(plugin)})
return result 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))

View File

@ -4,23 +4,33 @@ from .models import Mediafile
from .serializers import MediafileSerializer from .serializers import MediafileSerializer
# Viewsets for the REST API
class MediafileViewSet(ModelViewSet): class MediafileViewSet(ModelViewSet):
""" """
API endpoint to list, retrieve, create, update and destroy mediafile API endpoint for mediafile objects.
objects.
There are the following views: list, retrieve, create, partial_update,
update and destroy.
""" """
queryset = Mediafile.objects.all() queryset = Mediafile.objects.all()
serializer_class = MediafileSerializer serializer_class = MediafileSerializer
def check_permissions(self, request): def check_view_permissions(self):
""" """
Calls self.permission_denied() if the requesting user has not the Returns True if the user has required permissions.
permission to see mediafile objects and in case of create, update or
destroy requests the permission to manage mediafile objects.
""" """
# TODO: Use mediafiles.can_upload permission to create and update some # TODO: Use mediafiles.can_upload permission to create and update some
# objects but restricted concerning the uploader. # objects but restricted concerning the uploader.
if (not request.user.has_perm('mediafiles.can_see') or if self.action in ('list', 'retrieve'):
(self.action in ('create', 'update', 'destroy') and not result = self.request.user.has_perm('mediafiles.can_see')
request.user.has_perm('mediafiles.can_manage'))): elif self.action in ('create', 'partial_update', 'update'):
self.permission_denied(request) 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

View File

@ -24,40 +24,45 @@ from .serializers import (
) )
# Viewsets for the REST API
class MotionViewSet(ModelViewSet): 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() queryset = Motion.objects.all()
serializer_class = MotionSerializer serializer_class = MotionSerializer
def check_permissions(self, request): def check_view_permissions(self):
""" """
Calls self.permission_denied() if the requesting user has not the Returns True if the user has required permissions.
permission to see motions and in case of destroy requests the
permission to manage motions.
""" """
if (not request.user.has_perm('motions.can_see') or if self.action in ('list', 'retrieve', 'partial_update', 'update'):
(self.action == 'destroy' and not request.user.has_perm('motions.can_manage'))): result = self.request.user.has_perm('motions.can_see')
self.permission_denied(request) # 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): def create(self, request, *args, **kwargs):
""" """
Customized view endpoint to create a new motion. 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. # Check permission to send submitter and supporter data.
if (not request.user.has_perm('motions.can_manage') and if (not request.user.has_perm('motions.can_manage') and
(request.data.getlist('submitters') or request.data.getlist('supporters'))): (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 Checks also whether the requesting user can update the motion. He
needs at least the permissions 'motions.can_see' (see 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_allowed_actions() is evaluated.
""" """
# Get motion. # Get motion.
@ -122,10 +127,6 @@ class MotionViewSet(ModelViewSet):
{'version_number': <number>} to delete a version. Deleting the {'version_number': <number>} to delete a version. Deleting the
active version is not allowed. Only managers can use this view. 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. # Retrieve motion and version.
motion = self.get_object() motion = self.get_object()
version_number = request.data.get('version_number') version_number = request.data.get('version_number')
@ -168,17 +169,7 @@ class MotionViewSet(ModelViewSet):
(unsupport). (unsupport).
Send POST to support and DELETE to 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. # Retrieve motion and allowed actions.
motion = self.get_object() motion = self.get_object()
allowed_actions = motion.get_allowed_actions(request.user) allowed_actions = motion.get_allowed_actions(request.user)
@ -211,10 +202,6 @@ class MotionViewSet(ModelViewSet):
Send PUT {'state': <state_id>} to set and just PUT {} to reset the Send PUT {'state': <state_id>} to set and just PUT {} to reset the
state. Only managers can use this view. 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. # Retrieve motion and state.
motion = self.get_object() motion = self.get_object()
state = request.data.get('state') state = request.data.get('state')
@ -245,6 +232,56 @@ class MotionViewSet(ModelViewSet):
return Response({'detail': message}) 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): class PollPDFView(PDFView):
""" """
Generates a ballotpaper. Generates a ballotpaper.
@ -349,41 +386,3 @@ class MotionPDFView(SingleObjectMixin, PDFView):
motions_to_pdf(pdf, motions) motions_to_pdf(pdf, motions)
else: else:
motion_to_pdf(pdf, self.get_object()) 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)

View File

@ -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 # Views to generate PDFs
class UsersListPDF(PDFView): class UsersListPDF(PDFView):
@ -51,146 +194,3 @@ class UsersPasswordsPDF(PDFView):
Append PDF objects. Append PDF objects.
""" """
users_passwords_to_pdf(pdf) 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)

View File

@ -1,8 +1,7 @@
import re import re
from urllib.parse import urlparse from urllib.parse import urlparse
from rest_framework.decorators import detail_route # noqa from rest_framework.decorators import detail_route, list_route # noqa
from rest_framework.decorators import list_route # noqa
from rest_framework.metadata import SimpleMetadata # noqa from rest_framework.metadata import SimpleMetadata # noqa
from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa
from rest_framework.response import Response # noqa from rest_framework.response import Response # noqa
@ -20,39 +19,43 @@ from rest_framework.serializers import ( # noqa
SerializerMethodField, SerializerMethodField,
ValidationError, ValidationError,
) )
from rest_framework.viewsets import GenericViewSet as _GenericViewSet # noqa
from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa
from rest_framework.viewsets import ( # noqa from rest_framework.viewsets import \
GenericViewSet, ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa
ReadOnlyModelViewSet, from rest_framework.viewsets import ViewSet as _ViewSet # noqa
ViewSet,
)
from .exceptions import OpenSlidesError from .exceptions import OpenSlidesError
router = DefaultRouter() router = DefaultRouter()
class ModelViewSet(_ModelViewSet): class PermissionMixin:
""" """
Viewset for models. Before the method check_permission is called we Mixin for subclasses of APIView like GenericViewSet and ModelViewSet.
check projector requirements. If access for projector client users is
not currently required, check_permission is called, else not.
"""
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 The methods check_view_permissions or check_projector_requirements are
self.perform_authentication(request) evaluated. If both return False self.permission_denied() is called.
if not self.check_projector_requirements(): Django REST framework's permission system is disabled.
self.check_permissions(request) """
self.check_throttles(request)
# Perform content negotiation and store the accepted info on the request def get_permissions(self):
neg = self.perform_content_negotiation(request) """
request.accepted_renderer, request.accepted_media_type = neg 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 ()
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): def check_projector_requirements(self):
""" """
@ -70,6 +73,22 @@ class ModelViewSet(_ModelViewSet):
return result 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): def get_collection_and_id_from_url(url):
""" """
Helper function. Returns a tuple containing the collection name and the id Helper function. Returns a tuple containing the collection name and the id

View File

@ -1,8 +1,7 @@
from io import BytesIO from io import BytesIO
from django.conf import settings
from django.core.exceptions import PermissionDenied 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.utils.translation import ugettext_lazy
from django.views import generic as django_views from django.views import generic as django_views
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
@ -16,41 +15,6 @@ from .pdf import firstPage, laterPages
View = django_views.View 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): class SingleObjectMixin(django_views.detail.SingleObjectMixin):
""" """
Mixin for single objects from the database. Mixin for single objects from the database.
@ -88,14 +52,33 @@ class CSRFMixin:
return ensure_csrf_cookie(view) return ensure_csrf_cookie(view)
class PDFView(PermissionMixin, View): class PDFView(View):
""" """
View to generate an PDF. View to generate an PDF.
""" """
filename = ugettext_lazy('undefined-filename') filename = ugettext_lazy('undefined-filename')
top_space = 3 top_space = 3
document_title = None 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): def get_top_space(self):
return self.top_space return self.top_space

View File

@ -1,67 +1,9 @@
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from django.core.exceptions import PermissionDenied
from openslides.utils import views 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') @patch('builtins.super')
class SingleObjectMixinTest(TestCase): class SingleObjectMixinTest(TestCase):
def test_get_object_cache(self, mock_super): def test_get_object_cache(self, mock_super):