Add unit tests to check the issues found in the bug bounty report

This commit is contained in:
Ramon Wenger 2021-11-02 12:18:05 +01:00 committed by Christian Cueni
parent 79cd70cbd8
commit 338e4cfcfc
9 changed files with 162 additions and 34 deletions

View File

@ -1,4 +1,5 @@
import graphene import graphene
from graphql_relay import to_global_id
class HiddenForMixin: class HiddenForMixin:
@ -18,3 +19,12 @@ class VisibleForMixin:
class HiddenAndVisibleForMixin(HiddenForMixin, VisibleForMixin): class HiddenAndVisibleForMixin(HiddenForMixin, VisibleForMixin):
pass pass
class GraphqlNodeMixin:
def default_node_name(self):
return f'{self.__class__.__name__}Node'
@property
def graphql_id(self):
return to_global_id(self.default_node_name(), self.id)

View File

@ -3,17 +3,9 @@ from django.db import models
from django_extensions.db.models import TitleSlugDescriptionModel from django_extensions.db.models import TitleSlugDescriptionModel
from graphql_relay import to_global_id from graphql_relay import to_global_id
from core.mixins import GraphqlNodeMixin
from users.models import User from users.models import User
class GraphqlNodeMixin:
def default_node_name(self):
return f'{self.__class__.__name__}Node'
@property
def graphql_id(self):
return to_global_id(self.default_node_name(), self.id)
class Project(TitleSlugDescriptionModel, GraphqlNodeMixin): class Project(TitleSlugDescriptionModel, GraphqlNodeMixin):
objectives = models.TextField(blank=True) objectives = models.TextField(blank=True)
appearance = models.CharField(blank=True, null=False, max_length=255) appearance = models.CharField(blank=True, null=False, max_length=255)

View File

@ -6,6 +6,7 @@ from graphene_django import DjangoObjectType
from api.utils import get_by_id_or_slug from api.utils import get_by_id_or_slug
from portfolio.models import Project, ProjectEntry from portfolio.models import Project, ProjectEntry
from users.models import Role, UserRole, User from users.models import Role, UserRole, User
from users.schema import PublicUserNode
class ProjectEntryNode(DjangoObjectType): class ProjectEntryNode(DjangoObjectType):
@ -19,6 +20,7 @@ class ProjectNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
entries_count = graphene.Int() entries_count = graphene.Int()
entries = graphene.List(ProjectEntryNode) entries = graphene.List(ProjectEntryNode)
owner = graphene.Field(PublicUserNode)
class Meta: class Meta:
model = Project model = Project

View File

@ -3,6 +3,7 @@ from graphene.test import Client
from graphql_relay import to_global_id from graphql_relay import to_global_id
from api.schema import schema from api.schema import schema
from core.tests.base_test import SkillboxTestCase
from portfolio.factories import ProjectFactory from portfolio.factories import ProjectFactory
from users.factories import SchoolClassFactory from users.factories import SchoolClassFactory
from users.models import User from users.models import User
@ -10,7 +11,7 @@ from users.services import create_users
from api.test_utils import create_client, DefaultUserTestCase from api.test_utils import create_client, DefaultUserTestCase
from portfolio.models import Project from portfolio.models import Project
class ProjectQuery(TestCase): class ProjectQuery(SkillboxTestCase):
def setUp(self): def setUp(self):
create_users() create_users()
self.teacher = User.objects.get(username='teacher') self.teacher = User.objects.get(username='teacher')
@ -49,7 +50,6 @@ class ProjectQuery(TestCase):
self.assertEqual(Project.objects.count(), 0) self.assertEqual(Project.objects.count(), 0)
def test_should_not_be_able_to_delete_other_projects(self): def test_should_not_be_able_to_delete_other_projects(self):
self.assertEqual(Project.objects.count(), 1) self.assertEqual(Project.objects.count(), 1)
request = RequestFactory().get('/') request = RequestFactory().get('/')
request.user = self.student2 request.user = self.student2
@ -58,6 +58,41 @@ class ProjectQuery(TestCase):
result = self.client.execute(self.mutation, variables=self.variables) result = self.client.execute(self.mutation, variables=self.variables)
self.assertEqual(result.get('errors')[0]['message'], 'Permission denied: Incorrect project') self.assertEqual(result.get('errors')[0]['message'], 'Permission denied: Incorrect project')
def test_should_not_be_able_to_edit_other_projects(self):
self.assertEqual(Project.objects.count(), 1)
request = RequestFactory().get('/')
request.user = self.student2
self.client = Client(schema=schema, context_value=request)
mutation = '''
mutation UpdateProjectMutation($input: UpdateProjectInput!){
updateProject(input: $input) {
project {
id
}
}
}
'''
# project:
# {
# title: String
# description: String
# objectives: String
# appearance: String
# id: ID!
# final: Boolean
# }
input = {
'project': {
'id': self.project1.graphql_id,
'title': 'BAD! THIS IS BAD!'
}
}
result = self.get_client(self.student2).get_result(mutation, variables={
'input': input
})
self.assertIsNotNone(result.errors)
self.assertTrue('Permission' in result.errors)
class ProjectMutationsTestCase(DefaultUserTestCase): class ProjectMutationsTestCase(DefaultUserTestCase):
def test_add_project(self): def test_add_project(self):

View File

@ -1,3 +1,5 @@
from graphql_relay import to_global_id
from core.tests.base_test import SkillboxTestCase from core.tests.base_test import SkillboxTestCase
from portfolio.factories import ProjectFactory from portfolio.factories import ProjectFactory
from portfolio.models import Project from portfolio.models import Project
@ -13,7 +15,7 @@ query ProjectQuery($id: ID!) {
""" """
class ProjectQueryTestCaswe(SkillboxTestCase): class ProjectQueryTestCase(SkillboxTestCase):
def _test_direct_project_access(self, user: User, should_have_access: bool): def _test_direct_project_access(self, user: User, should_have_access: bool):
result = self.get_client(user).get_result(project_query, variables={ result = self.get_client(user).get_result(project_query, variables={
'id': self.project1.graphql_id 'id': self.project1.graphql_id
@ -30,6 +32,7 @@ class ProjectQueryTestCaswe(SkillboxTestCase):
school_class2 = SchoolClassFactory(users=[self.teacher2, self.student2]) school_class2 = SchoolClassFactory(users=[self.teacher2, self.student2])
self.project1 = ProjectFactory(student=self.student1) self.project1 = ProjectFactory(student=self.student1)
self.project1_id = to_global_id('ProjectNode', self.project1.id)
self.query = ''' self.query = '''
query ProjectsQuery { query ProjectsQuery {
projects { projects {
@ -113,3 +116,19 @@ class ProjectQueryTestCaswe(SkillboxTestCase):
self._test_direct_project_access(self.teacher2, False) self._test_direct_project_access(self.teacher2, False)
# non-owner can't access project # non-owner can't access project
self._test_direct_project_access(self.student2, False) self._test_direct_project_access(self.student2, False)
def test_project_owner(self):
query = """
query ProjectQuery($id: ID!) {
project(id: $id) {
id
owner {
email
}
}
}
"""
result = self.get_client(self.student1).get_result(query, variables={
'id': self.project1.graphql_id
})
self.assertIsNotNone(result.errors)

View File

@ -5,10 +5,11 @@ from wagtail.core.fields import StreamField
from books.blocks import DocumentBlock, ImageUrlBlock, LinkBlock, VideoBlock from books.blocks import DocumentBlock, ImageUrlBlock, LinkBlock, VideoBlock
from books.models import TextBlock from books.models import TextBlock
from core.mixins import GraphqlNodeMixin
from users.models import SchoolClass from users.models import SchoolClass
class Room(TitleSlugDescriptionModel): class Room(TitleSlugDescriptionModel, GraphqlNodeMixin):
class Meta: class Meta:
verbose_name = 'Raum' verbose_name = 'Raum'
verbose_name_plural = 'Räume' verbose_name_plural = 'Räume'

View File

@ -2,6 +2,8 @@ from graphql_relay import from_global_id
from core.tests.base_test import SkillboxTestCase from core.tests.base_test import SkillboxTestCase
from core.tests.helpers import GQLResult from core.tests.helpers import GQLResult
from rooms.models import Room
class GQLRoom: class GQLRoom:
def __init__(self, room_data): def __init__(self, room_data):
@ -30,29 +32,30 @@ class AddRoomResult:
class NewRoomMutationTestCase(SkillboxTestCase): class NewRoomMutationTestCase(SkillboxTestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.createDefault() self.createDefault()
self.mutation = """
mutation AddRoom($input: AddRoomInput!){
addRoom(input: $input) {
room {
id
slug
title
entryCount
appearance
description
schoolClass {
id
name
}
}
}
}
"""
def test_create_new_room(self): def test_create_new_room(self):
mutation = """ self.assertEqual(Room.objects.count(), 0)
mutation AddRoom($input: AddRoomInput!){
addRoom(input: $input) {
room {
id
slug
title
entryCount
appearance
description
schoolClass {
id
name
}
}
}
}
"""
title = 'some title' title = 'some title'
appearance='blue' appearance='blue'
res = self.get_client().execute(mutation, variables={ res = self.get_client().execute(self.mutation, variables={
'input': { 'input': {
'room': { 'room': {
'title': title, 'title': title,
@ -68,5 +71,23 @@ mutation AddRoom($input: AddRoomInput!){
self.assertEqual(room.appearance, appearance) self.assertEqual(room.appearance, appearance)
self.assertIsNone(room.description) self.assertIsNone(room.description)
self.assertEqual(int(from_global_id(room.school_class.get('id'))[1]), self.teacher.selected_class.id) self.assertEqual(int(from_global_id(room.school_class.get('id'))[1]), self.teacher.selected_class.id)
self.assertEqual(Room.objects.count(), 1)
def test_create_new_room_for_other_school_class(self):
self.assertEqual(Room.objects.count(), 0)
result = self.get_client(self.teacher2).get_result(self.mutation, variables={
'input': {
'room': {
'title': 'BIG NO NO!',
# description
'schoolClass': {
'id': self.school_class.graphql_id
},
'appearance': 'red'
}
}
})
self.assertIsNotNone(result.errors)
self.assertTrue('Permission' in result.errors)
self.assertEqual(Room.objects.count(), 0)

View File

@ -4,12 +4,13 @@ from graphql_relay import to_global_id
from api.schema import schema from api.schema import schema
from core.factories import UserFactory from core.factories import UserFactory
from core.tests.base_test import SkillboxTestCase
from rooms.factories import RoomEntryFactory, RoomFactory from rooms.factories import RoomEntryFactory, RoomFactory
from rooms.models import RoomEntry from rooms.models import RoomEntry
from users.factories import SchoolClassFactory from users.factories import SchoolClassFactory
class RoomEntryMutationsTestCase(TestCase): class RoomEntryMutationsTestCase(SkillboxTestCase):
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')
@ -17,6 +18,8 @@ class RoomEntryMutationsTestCase(TestCase):
s = SchoolClassFactory(users=[self.user, self.another_user]) s = SchoolClassFactory(users=[self.user, self.another_user])
s2 = SchoolClassFactory(users=[self.yet_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))
self.room = self.room_entry.room
self.first_school_class = s
request = RequestFactory().get('/') request = RequestFactory().get('/')
request.user = self.user request.user = self.user
@ -135,3 +138,47 @@ class RoomEntryMutationsTestCase(TestCase):
entry = RoomEntry.objects.get(pk=self.room_entry.pk) entry = RoomEntry.objects.get(pk=self.room_entry.pk)
self.assertIsNotNone(result.get('errors')) self.assertIsNotNone(result.get('errors'))
self.assertEqual(entry.title, self.room_entry.title) self.assertEqual(entry.title, self.room_entry.title)
def test_add_room_entry_not_owner_from_other_class(self):
self.assertEqual(RoomEntry.objects.count(), 1)
mutation = """
fragment RoomEntryParts on RoomEntryNode {
id
slug
title
contents
author {
id
firstName
lastName
avatarUrl
}
}
mutation AddRoomEntry($input: AddRoomEntryInput!){
addRoomEntry(input: $input) {
roomEntry {
...RoomEntryParts
}
errors
}
}
"""
# input:
# title = graphene.String(required=True)
# contents = graphene.List(ContentElementInput)
# room = graphene.ID(required=True)
room_entry = {
'title': 'Bad Actor!',
'room': self.room.graphql_id
}
result = self.get_client(self.yet_another_user).get_result(mutation, variables={
'input': {
'roomEntry': room_entry
}
})
self.assertIsNotNone(result.errors)
self.assertTrue('Permission' in result.errors)

View File

@ -14,6 +14,7 @@ from django.utils.functional import cached_property
from django.utils.timezone import is_aware, make_aware from django.utils.timezone import is_aware, make_aware
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from core.mixins import GraphqlNodeMixin
from users.licenses import MYSKILLBOX_LICENSES from users.licenses import MYSKILLBOX_LICENSES
from users.managers import LicenseManager, RoleManager, UserManager, UserRoleManager from users.managers import LicenseManager, RoleManager, UserManager, UserRoleManager
@ -176,7 +177,7 @@ class Team(GroupWithCode):
return self.name return self.name
class SchoolClass(GroupWithCode): class SchoolClass(GroupWithCode, GraphqlNodeMixin):
users = models.ManyToManyField(get_user_model(), related_name='school_classes', blank=True, users = models.ManyToManyField(get_user_model(), related_name='school_classes', blank=True,
through='users.SchoolClassMember') through='users.SchoolClassMember')