Skip to content

Commit

Permalink
overhaul permissions re #79
Browse files Browse the repository at this point in the history
  • Loading branch information
jkeifer committed Nov 14, 2017
1 parent 936e804 commit 9d46c79
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 67 deletions.
3 changes: 0 additions & 3 deletions ebagis/data/views/aoi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer, BrowsableAPIRenderer
from rest_framework.parsers import JSONParser, FormParser, MultiPartParser
from rest_framework.decorators import permission_classes
from rest_framework.permissions import AllowAny

from ...renderers import GeoJSONRenderer

Expand All @@ -28,7 +26,6 @@
)


@permission_classes((AllowAny, ))
class AOIViewSet(UpdateMixin, DownloadMixin, MultiSerializerMixin,
viewsets.ModelViewSet):
"""
Expand Down
3 changes: 0 additions & 3 deletions ebagis/data/views/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from __future__ import absolute_import

from rest_framework import viewsets
from rest_framework.decorators import permission_classes
from rest_framework.permissions import AllowAny

from ...views.filters import make_model_filter

Expand All @@ -14,7 +12,6 @@
# via the super function.


@permission_classes((AllowAny, ))
class BaseViewSet(viewsets.ModelViewSet):
_filter_args = {}
_prefetch_related_fields = None
Expand Down
1 change: 0 additions & 1 deletion ebagis/data/views/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ class UpdateMixin(object):
updated, passing in that object's id so the
upload knows that this is an update, not a create.
"""
@detail_route
def update(self, request, *args, **kwargs):
object = self.get_object()
return UploadView.new_upload(self.queryset.model,
Expand Down
4 changes: 0 additions & 4 deletions ebagis/data/views/pourpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
from rest_framework import viewsets
from rest_framework.renderers import JSONRenderer, BrowsableAPIRenderer
from rest_framework.response import Response
from rest_framework.decorators import permission_classes
from rest_framework.permissions import AllowAny

from ...renderers import GeoJSONRenderer

Expand All @@ -16,7 +14,6 @@
from ..serializers.aoi import AOIListSerializer


@permission_classes((AllowAny, ))
class PourPointViewSet(viewsets.ModelViewSet):
serializer_class = PourPointSerializer
renderer_classes = (JSONRenderer, BrowsableAPIRenderer, GeoJSONRenderer)
Expand All @@ -39,7 +36,6 @@ def aois(self, request, *args, **kwargs):
return Response(serializer.data)


@permission_classes((AllowAny, ))
class PourPointBoundaryViewSet(viewsets.ModelViewSet):
serializer_class = PourPointBoundarySerializer
renderer_classes = (JSONRenderer, BrowsableAPIRenderer, GeoJSONRenderer)
Expand Down
115 changes: 93 additions & 22 deletions ebagis/permissions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
from rest_framework import permissions


ADMIN_METHODS = ('DELETE', )
READ_ACTIONS = [
'list',
'retrieve'
]
AUTHENTICATED_ACTIONS = READ_ACTIONS + [
'download'
]
WRITE_ACTIONS = AUTHENTICATED_ACTIONS + [
'create',
'update',
'partial_update',
]
ADMIN_ACTIONS = WRITE_ACTIONS + [
'destroy',
]


class IsOwner(permissions.BasePermission):
Expand All @@ -17,30 +31,87 @@ def has_object_permission(self, request, view, obj, user_field=None):
return request.user == obj_user


class IsAdminOrStaffOrAuthenticated(permissions.BasePermission):
"""
The authenticated request is from a staff member,
or is a read-only request.
"""
class IsOwnerOrAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj, user_field=None):
if not (request.user and request.user.is_authenticated()):
return False

def has_permission(self, request, view):
if (
# authenticated user trying to GET, HEAD, or OPTIONS
(request.method in permissions.SAFE_METHODS and
request.user.groups.filter(name='NWCC_ADMIN').exists() or
request.user.is_superuser
):
return True

if user_field:
obj_user = getattr(obj, user_field)
else:
try:
obj_user = getattr(obj, 'created_by')
except AttributeError:
obj_user = getattr(obj, 'user')

return request.user == obj_user


class IsAdminOrNwccReadOnly(permissions.BasePermission):
'''
Only allow access to NWCC Users with write perms reserved for admins.
'''
def has_permission(self, request, view):
return (
# staff user
(view.action in READ_ACTIONS and
request.user and
request.user.is_authenticated() and
request.user.groups.filter(name='NWCC_STAFF').exists())
or
# admin user
(view.action in ADMIN_ACTIONS and
request.user and
request.user.is_authenticated() and
(request.user.groups.filter(name='NWCC_ADMIN').exists() or
request.user.is_superuser))
)


class IsNwccWrite(permissions.BasePermission):
'''
Let anyone from NWCC do anything
'''

def has_permission(self, request, view):
return (request.user and
request.user.is_authenticated() and
(request.user.groups.filter(
name__in=['NWCC_ADMIN', 'NWCC_STAFF']
).exists() or
request.user.is_superuser))


class CheckAdminStaffAuthOrAnon(permissions.BasePermission):
'''
Only allow actions suitable with user permission level.
'''
def has_permission(self, request, view):
return (
# any user
(view.action in READ_ACTIONS)
or
# authenticated user
(view.action in AUTHENTICATED_ACTIONS and
request.user and
request.user.is_authenticated())
or
# admin user trying to DELETE
(request.method in ADMIN_METHODS and
request.user and
request.user.is_superuser)
# staff user
(view.action in WRITE_ACTIONS and
request.user and
request.user.is_authenticated() and
request.user.groups.filter(name='NWCC_STAFF').exists())
or
# staff user trying to do anthing else
(request.user and
request.user.is_authenticated() and
request.user.is_staff)
):
return True
# user not authenticated or trying to do something
# without sufficient permission
return False
# admin user
(view.action in ADMIN_ACTIONS and
request.user and
request.user.is_authenticated() and
(request.user.groups.filter(name='NWCC_ADMIN').exists() or
request.user.is_superuser))
)
8 changes: 0 additions & 8 deletions ebagis/rest_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,16 @@

group_list = views.GroupViewSet.as_view({
"get": "list",
"post": "create",
})
group_detail = views.GroupViewSet.as_view({
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
})

permission_list = views.PermissionViewSet.as_view({
"get": "list",
"post": "create",
})
permission_detail = views.PermissionViewSet.as_view({
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
})

aoi_list = data_views.AOIViewSet.as_view({
Expand Down
5 changes: 3 additions & 2 deletions ebagis/settings/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.DjangoModelPermissions',
'rest_framework.permissions.IsAdminUser',
'ebagis.permissions.CheckAdminStaffAuthOrAnon',
#'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
#'rest_framework.permissions.IsAdminUser',
),
'PAGINATE_BY': 100,
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
Expand Down
37 changes: 31 additions & 6 deletions ebagis/utils/queries.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,50 @@
from __future__ import absolute_import

from django.http import Http404


def get_object_owner_or_admin(self):
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

assert lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg)
)

query = {self.lookup_field: self.kwargs[lookup_url_kwarg]}

if not (self.request.user.groups.filter(name='NWCC_ADMIN').exists() or
self.request.user.is_superuser):
query['user'] = self.request.user
try:
return self.model.objects.get(**query)
except self.model.DoesNotExist:
raise Http404


def owner_or_admin(queryset, request,
user_field='user', restrict_to_admin=True):
"""
Filter a queryset. Default is to show only records where user field
is the current user, unless admin and explicitly requesting all
matching records. THe user field is by default 'user', but that can
matching records. The user field is by default 'user', but that can
be changed as required.
"""

user = request.user

if "show_all" in request.query_params and \
(not restrict_to_admin or user.is_staff or user.is_admin):
if user.is_anonymous:
# we don't know who they are, so we give them nothing
return queryset.none()
elif ('show_all' in request.query_params and
(not restrict_to_admin or
user.groups.filter(name='NWCC_ADMIN').exists() or
user.is_superuser)):
# the user wants to see all records,
# so don't filter by user
return queryset
elif user.is_anonymous:
# we don't know who they are, so we give them nothing
return queryset.none()
else:
# normal behavior is to filter by user
return queryset.filter(**{user_field: user})
8 changes: 5 additions & 3 deletions ebagis/views/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import permission_classes

from djcelery.models import TaskMeta

Expand All @@ -21,17 +20,20 @@

# utils
from ..utils.http import stream_file
from ..utils.queries import owner_or_admin
from ..utils.queries import owner_or_admin, get_object_owner_or_admin

from .filters import make_model_filter


@permission_classes((IsAuthenticated, ))
class DownloadViewSet(viewsets.ModelViewSet):
model = Download
serializer_class = DownloadSerializer
search_fields = ("name",)
filter_class = make_model_filter(model, exclude_fields=['file'])
permission_classes = (IsAuthenticated, )

def get_object(self):
return get_object_owner_or_admin(self)

def get_queryset(self):
query = self.model.objects.all()
Expand Down
29 changes: 18 additions & 11 deletions ebagis/views/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.contrib.contenttypes.models import ContentType

from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework import status

from drf_chunked_upload.views import ChunkedUploadView
Expand All @@ -18,9 +18,9 @@

# other
from ..tasks import process_upload
from ..permissions import IsNwccWrite, IsOwnerOrAdmin
from ..utils.validation import generate_uuid
from ..utils.queries import owner_or_admin
from ..permissions import IsAdminOrStaffOrAuthenticated
from ..utils.queries import owner_or_admin, get_object_owner_or_admin

from .filters import (
CreatedAtMixin, FilterSet, make_model_filter, filters
Expand All @@ -43,17 +43,21 @@ class Meta:
abstract = True


# TODO: make into detail_route on UploadView
# but have to rework drf-chunked-upload to make
# ChunkedUploadView a viewset, not a class-based view
@api_view(['POST'])
@permission_classes((IsOwnerOrAdmin,))
def cancel_upload(request, pk):
query = {'pk': pk}
if not (request.user.groups.filter(name='NWCC_ADMIN').exists() or
request.user.is_superuser):
# non-admin users can only get their own uploads
query['user'] = request.user

try:
if request.user.is_superuser:
# allow admin users to get any user's upload
upload = Upload.objects.get(pk=pk)
else:
# normal users can only get their own uploads
upload = Upload.objects.get(pk=pk, user=request.user)
except upload.DoesNotExist:
upload = Upload.objects.get(**query)
except Upload.DoesNotExist:
# per queries above, this could be thrown even
# if an upload exists such as when a user tries
# to call this on an upload that is not theirs
Expand All @@ -77,14 +81,17 @@ class UploadView(ChunkedUploadView):
filter_class = make_model_filter(model,
base=UploadFilterSet,
exclude_fields=['file'])
permission_classes = (IsAdminOrStaffOrAuthenticated,)
permission_classes = (IsNwccWrite,)

def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request is None or self.request.method not in ['PUT', 'POST']:
serializer_class = UploadCreateSerializer
return serializer_class

def get_object(self):
return get_object_owner_or_admin(self)

def get_queryset(self):
"""
Get (and filter) Upload queryset.
Expand Down
Loading

0 comments on commit 9d46c79

Please sign in to comment.