diff --git a/server/core/mixins.py b/server/core/mixins.py index f444085e..4fb87c0b 100644 --- a/server/core/mixins.py +++ b/server/core/mixins.py @@ -1,4 +1,5 @@ import graphene +from graphql_relay import to_global_id class HiddenForMixin: @@ -18,3 +19,12 @@ class VisibleForMixin: class HiddenAndVisibleForMixin(HiddenForMixin, VisibleForMixin): 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) diff --git a/server/portfolio/models.py b/server/portfolio/models.py index 7cae2516..17ad7177 100644 --- a/server/portfolio/models.py +++ b/server/portfolio/models.py @@ -3,17 +3,9 @@ from django.db import models from django_extensions.db.models import TitleSlugDescriptionModel from graphql_relay import to_global_id +from core.mixins import GraphqlNodeMixin 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): objectives = models.TextField(blank=True) appearance = models.CharField(blank=True, null=False, max_length=255) diff --git a/server/portfolio/schema.py b/server/portfolio/schema.py index c647b510..652f86fe 100644 --- a/server/portfolio/schema.py +++ b/server/portfolio/schema.py @@ -6,6 +6,7 @@ from graphene_django import DjangoObjectType from api.utils import get_by_id_or_slug from portfolio.models import Project, ProjectEntry from users.models import Role, UserRole, User +from users.schema import PublicUserNode class ProjectEntryNode(DjangoObjectType): @@ -19,6 +20,7 @@ class ProjectNode(DjangoObjectType): pk = graphene.Int() entries_count = graphene.Int() entries = graphene.List(ProjectEntryNode) + owner = graphene.Field(PublicUserNode) class Meta: model = Project diff --git a/server/portfolio/tests/test_project_mutations.py b/server/portfolio/tests/test_project_mutations.py index ddcd3638..ce583191 100644 --- a/server/portfolio/tests/test_project_mutations.py +++ b/server/portfolio/tests/test_project_mutations.py @@ -3,6 +3,7 @@ from graphene.test import Client from graphql_relay import to_global_id from api.schema import schema +from core.tests.base_test import SkillboxTestCase from portfolio.factories import ProjectFactory from users.factories import SchoolClassFactory from users.models import User @@ -10,7 +11,7 @@ from users.services import create_users from api.test_utils import create_client, DefaultUserTestCase from portfolio.models import Project -class ProjectQuery(TestCase): +class ProjectQuery(SkillboxTestCase): def setUp(self): create_users() self.teacher = User.objects.get(username='teacher') @@ -49,7 +50,6 @@ class ProjectQuery(TestCase): self.assertEqual(Project.objects.count(), 0) def test_should_not_be_able_to_delete_other_projects(self): - self.assertEqual(Project.objects.count(), 1) request = RequestFactory().get('/') request.user = self.student2 @@ -58,6 +58,41 @@ class ProjectQuery(TestCase): result = self.client.execute(self.mutation, variables=self.variables) 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): def test_add_project(self): diff --git a/server/portfolio/tests/test_project_query.py b/server/portfolio/tests/test_project_query.py index b49c6452..b26e19d0 100644 --- a/server/portfolio/tests/test_project_query.py +++ b/server/portfolio/tests/test_project_query.py @@ -1,3 +1,5 @@ +from graphql_relay import to_global_id + from core.tests.base_test import SkillboxTestCase from portfolio.factories import ProjectFactory 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): result = self.get_client(user).get_result(project_query, variables={ 'id': self.project1.graphql_id @@ -30,6 +32,7 @@ class ProjectQueryTestCaswe(SkillboxTestCase): school_class2 = SchoolClassFactory(users=[self.teacher2, self.student2]) self.project1 = ProjectFactory(student=self.student1) + self.project1_id = to_global_id('ProjectNode', self.project1.id) self.query = ''' query ProjectsQuery { projects { @@ -113,3 +116,19 @@ class ProjectQueryTestCaswe(SkillboxTestCase): self._test_direct_project_access(self.teacher2, False) # non-owner can't access project 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) diff --git a/server/rooms/models.py b/server/rooms/models.py index d8d7d0e8..68f8c902 100644 --- a/server/rooms/models.py +++ b/server/rooms/models.py @@ -5,10 +5,11 @@ from wagtail.core.fields import StreamField from books.blocks import DocumentBlock, ImageUrlBlock, LinkBlock, VideoBlock from books.models import TextBlock +from core.mixins import GraphqlNodeMixin from users.models import SchoolClass -class Room(TitleSlugDescriptionModel): +class Room(TitleSlugDescriptionModel, GraphqlNodeMixin): class Meta: verbose_name = 'Raum' verbose_name_plural = 'Räume' diff --git a/server/rooms/tests/test_new_room_mutation.py b/server/rooms/tests/test_new_room_mutation.py index 8dcfb9c1..bba8da28 100644 --- a/server/rooms/tests/test_new_room_mutation.py +++ b/server/rooms/tests/test_new_room_mutation.py @@ -2,6 +2,8 @@ from graphql_relay import from_global_id from core.tests.base_test import SkillboxTestCase from core.tests.helpers import GQLResult +from rooms.models import Room + class GQLRoom: def __init__(self, room_data): @@ -30,29 +32,30 @@ class AddRoomResult: class NewRoomMutationTestCase(SkillboxTestCase): def setUp(self) -> None: 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): - mutation = """ -mutation AddRoom($input: AddRoomInput!){ - addRoom(input: $input) { - room { - id - slug - title - entryCount - appearance - description - schoolClass { - id - name - } - } - } -} -""" + self.assertEqual(Room.objects.count(), 0) title = 'some title' appearance='blue' - res = self.get_client().execute(mutation, variables={ + res = self.get_client().execute(self.mutation, variables={ 'input': { 'room': { 'title': title, @@ -68,5 +71,23 @@ mutation AddRoom($input: AddRoomInput!){ self.assertEqual(room.appearance, appearance) self.assertIsNone(room.description) 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) diff --git a/server/rooms/tests/test_room_entry_mutations.py b/server/rooms/tests/test_room_entry_mutations.py index d20f5c0a..e7b9f426 100644 --- a/server/rooms/tests/test_room_entry_mutations.py +++ b/server/rooms/tests/test_room_entry_mutations.py @@ -4,12 +4,13 @@ from graphql_relay import to_global_id from api.schema import schema from core.factories import UserFactory +from core.tests.base_test import SkillboxTestCase from rooms.factories import RoomEntryFactory, RoomFactory from rooms.models import RoomEntry from users.factories import SchoolClassFactory -class RoomEntryMutationsTestCase(TestCase): +class RoomEntryMutationsTestCase(SkillboxTestCase): def setUp(self): self.user = UserFactory(username='aschi') self.another_user = UserFactory(username='pesche') @@ -17,6 +18,8 @@ class RoomEntryMutationsTestCase(TestCase): 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 = self.room_entry.room + self.first_school_class = s request = RequestFactory().get('/') request.user = self.user @@ -135,3 +138,47 @@ class RoomEntryMutationsTestCase(TestCase): entry = RoomEntry.objects.get(pk=self.room_entry.pk) self.assertIsNotNone(result.get('errors')) 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) diff --git a/server/users/models.py b/server/users/models.py index 50a748ba..e87b88dd 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -14,6 +14,7 @@ from django.utils.functional import cached_property from django.utils.timezone import is_aware, make_aware from django.utils.translation import ugettext_lazy as _ +from core.mixins import GraphqlNodeMixin from users.licenses import MYSKILLBOX_LICENSES from users.managers import LicenseManager, RoleManager, UserManager, UserRoleManager @@ -176,7 +177,7 @@ class Team(GroupWithCode): return self.name -class SchoolClass(GroupWithCode): +class SchoolClass(GroupWithCode, GraphqlNodeMixin): users = models.ManyToManyField(get_user_model(), related_name='school_classes', blank=True, through='users.SchoolClassMember')