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
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': <speaker_id>} 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 = "&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):
"""
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': <id>}.
"""
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': <id>}.
"""
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

View File

@ -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))

View File

@ -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

View File

@ -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': <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': <state_id>} 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)

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
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)

View File

@ -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

View File

@ -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

View File

@ -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):