Merged in feature/testing (pull request #12)

restrict access to users and rooms, add tests

Approved-by: Ramon Wenger <ramon.wenger@iterativ.ch>
This commit is contained in:
Christian Cueni 2019-04-08 09:05:37 +00:00
commit e9895d9f74
11 changed files with 246 additions and 10 deletions

View File

@ -1,6 +1,7 @@
import graphene import graphene
from graphene import relay, InputObjectType from graphene import relay, InputObjectType
from graphql_relay import from_global_id from graphql_relay import from_global_id
from rest_framework.exceptions import PermissionDenied
from api.utils import get_object from api.utils import get_object
from books.models import Module from books.models import Module
@ -67,13 +68,18 @@ class AddObjectiveGroup(relay.ClientIDMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
owner = info.context.user
if not owner.has_perm('users.can_manage_school_class_content'):
raise PermissionDenied('Missing permissions')
objective_group_data = kwargs.get('objective_group') objective_group_data = kwargs.get('objective_group')
title = objective_group_data.get('title') title = objective_group_data.get('title')
if title != 'society': if title != 'society':
title = 'language_communication' title = 'language_communication'
module_id = objective_group_data.get('module') module_id = objective_group_data.get('module')
module = get_object(Module, module_id) module = get_object(Module, module_id)
owner = info.context.user
new_objective_group = ObjectiveGroup.objects.create(title=title, module=module, owner=owner) new_objective_group = ObjectiveGroup.objects.create(title=title, module=module, owner=owner)
objectives = objective_group_data.get('objectives') objectives = objective_group_data.get('objectives')
for objective in objectives: for objective in objectives:
@ -89,9 +95,15 @@ class UpdateObjectiveGroup(relay.ClientIDMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user
if not user.has_perm('users.can_manage_school_class_content'):
raise PermissionDenied('Missing permissions')
objective_group_data = kwargs.get('objective_group') objective_group_data = kwargs.get('objective_group')
id = objective_group_data.get('id') id = objective_group_data.get('id')
objective_group = get_object(ObjectiveGroup, id) objective_group = get_object(ObjectiveGroup, id)
objectives = objective_group_data.get('objectives') objectives = objective_group_data.get('objectives')
existing_objective_ids = list(objective_group.objectives.values_list('id', flat=True)) existing_objective_ids = list(objective_group.objectives.values_list('id', flat=True))
for objective in objectives: for objective in objectives:

View File

@ -26,7 +26,6 @@ class ObjectiveGroupNode(DjangoObjectType):
return self.owner is not None and self.owner.pk == info.context.user.pk return self.owner is not None and self.owner.pk == info.context.user.pk
class ObjectiveNode(DjangoObjectType): class ObjectiveNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()

View File

@ -1 +0,0 @@
# Create your tests here.

View File

View File

@ -1,10 +1,10 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django_extensions.db.models import TitleDescriptionModel, TitleSlugDescriptionModel from django_extensions.db.models import TitleSlugDescriptionModel
from wagtail.core.fields import StreamField from wagtail.core.fields import StreamField
from books.blocks import ImageUrlBlock, LinkBlock, VideoBlock from books.blocks import ImageUrlBlock, LinkBlock, VideoBlock
from books.models import ContentBlock, TextBlock from books.models import TextBlock
from users.models import SchoolClass from users.models import SchoolClass
@ -37,3 +37,6 @@ class RoomEntry(TitleSlugDescriptionModel):
def __str__(self): def __str__(self):
return 'RoomEntry {}-{}-{}'.format(self.id, self.title, self.author) return 'RoomEntry {}-{}-{}'.format(self.id, self.title, self.author)
def can_user_see_entry(self, user):
return user.is_superuser or self.room.school_class.is_user_in_schoolclass(user)

View File

@ -84,14 +84,24 @@ class MutateRoomEntry(relay.ClientIDMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
room_entry_data = kwargs.get('room_entry') room_entry_data = kwargs.get('room_entry')
if room_entry_data.get('room') is not None: if room_entry_data.get('room') is not None:
room_entry_data['room'] = get_object(Room, room_entry_data.get('room')).id room_entry_data['room'] = get_object(Room, room_entry_data.get('room')).id
room_entry_data['author'] = info.context.user.pk
if room_entry_data.get('id') is not None: if room_entry_data.get('id') is not None:
# update path
instance = get_object(RoomEntry, room_entry_data.get('id')) instance = get_object(RoomEntry, room_entry_data.get('id'))
if not instance.room.school_class.is_user_in_schoolclass(info.context.user):
raise Exception('You are in the wrong class')
if instance.author.pk != info.context.user.pk:
raise Exception('You are not the author')
serializer = RoomEntrySerializer(instance, data=room_entry_data, partial=True) serializer = RoomEntrySerializer(instance, data=room_entry_data, partial=True)
else: else:
# add path
room_entry_data['author'] = info.context.user.pk
serializer = RoomEntrySerializer(data=room_entry_data) serializer = RoomEntrySerializer(data=room_entry_data)
if serializer.is_valid(): if serializer.is_valid():

View File

@ -55,14 +55,29 @@ class RoomsQuery(object):
return Room.objects.filter(school_class__in=user.school_classes.all()) return Room.objects.filter(school_class__in=user.school_classes.all())
def resolve_room(self, info, **kwargs): def resolve_room(self, info, **kwargs):
return get_by_id_or_slug(Room, **kwargs) room = get_by_id_or_slug(Room, **kwargs)
if room.school_class.is_user_in_schoolclass(info.context.user):
return room
else:
return None
def resolve_room_entry(self, info, **kwargs): def resolve_room_entry(self, info, **kwargs):
slug = kwargs.get('slug') slug = kwargs.get('slug')
id = kwargs.get('id') id = kwargs.get('id')
room_entry = None
if id is not None: if id is not None:
return get_object(RoomEntry, id) room_entry = get_object(RoomEntry, id)
if slug is not None: if slug is not None:
return RoomEntry.objects.get(slug=slug) room_entry = RoomEntry.objects.get(slug=slug)
if room_entry and room_entry.can_user_see_entry(info.context.user):
return room_entry
else:
return None return None
def resolve_all_room_entries(self, info, **kwargs):
if not info.context.user.is_superuser:
return RoomEntry.objects.none()
else:
return RoomEntry.objects.all()

View File

@ -12,7 +12,9 @@ class RoomEntryMutationsTestCase(TestCase):
def setUp(self): def setUp(self):
self.user = UserFactory(username='aschi') self.user = UserFactory(username='aschi')
self.another_user = UserFactory(username='pesche') self.another_user = UserFactory(username='pesche')
self.yet_another_user = UserFactory(username='hansueli')
s = SchoolClassFactory(users=[self.user, self.another_user]) s = SchoolClassFactory(users=[self.user, self.another_user])
s2 = SchoolClassFactory(users=[self.yet_another_user])
self.room_entry = RoomEntryFactory(author=self.user, room=RoomFactory(school_class=s)) self.room_entry = RoomEntryFactory(author=self.user, room=RoomFactory(school_class=s))
request = RequestFactory().get('/') request = RequestFactory().get('/')
@ -62,3 +64,73 @@ class RoomEntryMutationsTestCase(TestCase):
self.assertIsNotNone(result.get('errors')) self.assertIsNotNone(result.get('errors'))
self.assertEqual(RoomEntry.objects.count(), 1) self.assertEqual(RoomEntry.objects.count(), 1)
def test_update_room_entry_not_owner_but_same_class(self):
self.assertEqual(RoomEntry.objects.count(), 1)
mutation = '''
mutation UpdateRoomEntry($input: UpdateRoomEntryInput!){
updateRoomEntry(input: $input) {
roomEntry {
title
author {
firstName
}
}
errors
}
}
'''
request = RequestFactory().get('/')
request.user = self.another_user
client = Client(schema=schema, context_value=request)
new_title = 'new title, Alte!'
result = client.execute(mutation, variables={
'input': {
'roomEntry': {
'id': to_global_id('RoomEntryNode', self.room_entry.pk),
'title': new_title
}
}
})
entry = RoomEntry.objects.get(pk=self.room_entry.pk)
self.assertIsNotNone(result.get('errors'))
self.assertEqual(entry.title, self.room_entry.title)
def test_update_room_entry_not_owner_from_other_class(self):
self.assertEqual(RoomEntry.objects.count(), 1)
mutation = '''
mutation UpdateRoomEntry($input: UpdateRoomEntryInput!){
updateRoomEntry(input: $input) {
roomEntry {
title
author {
firstName
}
}
errors
}
}
'''
request = RequestFactory().get('/')
request.user = self.yet_another_user
client = Client(schema=schema, context_value=request)
new_title = 'new title, Alte!'
result = client.execute(mutation, variables={
'input': {
'roomEntry': {
'id': to_global_id('RoomEntryNode', self.room_entry.pk),
'title': new_title
}
}
})
entry = RoomEntry.objects.get(pk=self.room_entry.pk)
self.assertIsNotNone(result.get('errors'))
self.assertEqual(entry.title, self.room_entry.title)

View File

@ -0,0 +1,117 @@
from django.test import TestCase, RequestFactory
from graphene.test import Client
from api.schema import schema
from core.factories import UserFactory
from rooms.factories import RoomFactory, RoomEntryFactory
from users.factories import SchoolClassFactory
class RoomQueryPermission(TestCase):
@staticmethod
def get_first_contents(result):
return result.get('data').get('rooms').get('edges')[0]
def setUp(self):
self.user = UserFactory(username='aschi')
self.another_user = UserFactory(username='pesche')
sc1 = SchoolClassFactory(users=[self.user])
sc2 = SchoolClassFactory(users=[self.another_user])
self.room1 = RoomFactory(school_class=sc1)
self.room2 = RoomFactory(school_class=sc2)
request = RequestFactory().get('/')
request.user = self.user
self.client = Client(schema=schema, context_value=request)
def test_student_should_only_see_rooms_of_class(self):
query = '''
query {
rooms {
edges {
node {
title
}
}
}
}
'''
result = self.client.execute(query)
self.assertIsNone(result.get('errors'))
self.assertEqual(len(result.get('data').get('rooms').get('edges')), 1)
self.assertEqual(result.get('data').get('rooms').get('edges')[0].get('node').get('title'), self.room1.title)
def test_student_should_not_be_able_to_query_rooms_of_other_classes(self):
query = '''
query RoomQuery($slug: String) {
room(slug: $slug) {
title
}
}
'''
result = self.client.execute(query, variables={
'slug': self.room2.slug
})
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('room'), None)
class RoomEntryQueryPermissions(TestCase):
@staticmethod
def get_first_contents(result):
return result.get('data').get('rooms').get('edges')[0]
def setUp(self):
self.user = UserFactory(username='aschi')
self.another_user = UserFactory(username='pesche')
sc1 = SchoolClassFactory(users=[self.user])
sc2 = SchoolClassFactory(users=[self.another_user])
room1 = RoomFactory(school_class=sc1)
room2 = RoomFactory(school_class=sc2)
self.roomEntry1 = RoomEntryFactory(room=room1, author=self.user)
self.roomEntry2 = RoomEntryFactory(room=room2, author=self.another_user)
request = RequestFactory().get('/')
request.user = self.user
self.client = Client(schema=schema, context_value=request)
def test_user_should_see_room_entries_from_own_class(self):
query = '''
query RoomEntryQuery($slug: String) {
roomEntry(slug: $slug) {
title
}
}
'''
result = self.client.execute(query, variables={
'slug': self.roomEntry1.slug
})
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('roomEntry').get('title'), self.roomEntry1.title)
def test_user_should_not_see_room_entries_from_orther_class(self):
query = '''
query RoomEntryQuery($slug: String) {
roomEntry(slug: $slug) {
title
}
}
'''
result = self.client.execute(query, variables={
'slug': self.roomEntry2.slug
})
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('roomEntry'), None)

View File

@ -51,6 +51,9 @@ class SchoolClass(models.Model):
def __str__(self): def __str__(self):
return 'SchoolClass {}-{}-{}'.format(self.id, self.name, self.year) return 'SchoolClass {}-{}-{}'.format(self.id, self.name, self.year)
def is_user_in_schoolclass(self, user):
return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0
class Role(models.Model): class Role(models.Model):
key = models.CharField(_('Key'), max_length=100, blank=False, null=False, unique=True) key = models.CharField(_('Key'), max_length=100, blank=False, null=False, unique=True)

View File

@ -45,3 +45,9 @@ class UsersQuery(object):
def resolve_me(self, info, **kwargs): def resolve_me(self, info, **kwargs):
return info.context.user return info.context.user
def resolve_all_users(self, info, **kwargs):
if not info.context.user.is_superuser:
return User.objects.none()
else:
return User.objects.all()