I'm currently rewriting my django-rest-framework implementation to use a custom permissions class. In the process, I am writing tests to make sure that any future changes don't break anything.
class EventViewSet(viewsets.ModelViewSet):
serializer_class = EventSerializer
permission_classes = [ReadOnly, CheckPermission]
def get_permissions(self):
# This passes the kwargs needed for the permission check to the permission class
if self.action in ['create'] and self.request.user.is_authenticated:
return [CheckPermission(capability='events', action='write')]
if self.action in ['retrieve', 'list']:
return [CheckPermission(capability='events', action='read')]
elif self.action in ['update', 'partial_update'] and self.request.user.is_authenticated:
return [CheckPermission(capability='events', action='edit', org=Organization.objects.get(short_name=self.request.data['organizationChange']))]
elif self.action == 'destroy' and self.request.user.is_authenticated:
return [CheckPermission(capability='events', action='edit')]
return super().get_permissions()
def get_queryset(self):
user = self.request.user
if 'org' in self.request.query_params:
org = Organization.objects.get(short_name=self.request.query_params['org'])
# check if user has necessary permissions for the specific organization
perm = CheckPermission(capability='event', org=org, action='read').has_permission(self.request, self)
if perm:
return Event.objects.filter(organization=org, removed=False)
else:
#return public events for that org
return Event.objects.filter(organization=org, type=0, removed=False)
else:
try:
q1 = Event.objects.filter(type=0, removed=False)
if user.is_authenticated:
memberships = AccountRoleMembership.objects.filter(user=user, removed=False, role__removed=False, role__capabilities__removed=False, role__capabilities__capability__name='event', role__capabilities__read=True)
orgs = get_orgs(memberships)
if orgs.count() > 0:
for org in orgs:
# check if user has necessary permissions for events with a specific organization
perm = CheckPermission(capability='event', org=org, action='read').has_permission(self.request, self)
if perm:
q1 = q1 | Event.objects.filter(organization=org, type=1, removed=False)
return q1
else:
return Event.objects.filter(type=0, removed=False)
except Exception:
return Event.objects.filter(type=0, removed=False)
def get_interested(self, event):
user = self.request.user
return user.is_authenticated and check_permission(user, event.organization, 'event', 'write')
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
my_interest = self.get_my_interest(instance)
interested = self.get_interested(instance)
serializer = self.get_serializer(instance, context={'request': request, 'my_interest': my_interest, 'interested': interested})
return Response(serializer.data)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
my_interests = {event.id: self.get_my_interest(event) for event in queryset}
interested = {event.id: self.get_interested(event) for event in queryset}
serializer = self.get_serializer(queryset, many=True, context={'request': request, 'my_interests': my_interests, 'interested': interested})
return Response(serializer.data)
def perform_create(self, serializer):
user = self.request.user
org = None
# check if user has necessary permissions to create events
perm = CheckPermission(capability='event', action='create').has_permission(self.request, self)
if 'organizationChange' in self.request.data:
try:
org = Organization.objects.get(short_name=self.request.data['organizationChange'])
except:
print('Organization does not exist')
pass
elif 'guild' in self.request.data:
try:
org = DiscordServer.objects.get(server_id=self.context['request'].data['guild']).org
except:
pass
if perm:
try:
newEvent = serializer.save(owner=user,organization=org, source='scorg')
discordEvent = create_event(newEvent)
if discordEvent is not None:
newEvent.discord_event_id = str(discordEvent)
newEvent.save()
except Event.DoesNotExist:
pass
else:
# return a more informative response to the client if the user does not have the necessary permissions
return Response({"error": perm}, status=403)
...
class EventSerializer(serializers.ModelSerializer):
owner = serializers.PrimaryKeyRelatedField(read_only=True)
organization = OrganizationStubSerializer(many=False, allow_null=True, required=False)
short_name = serializers.ReadOnlyField(source='organization.short_name', read_only=True)
canRead = serializers.SerializerMethodField()
canWrite = serializers.SerializerMethodField()
canEdit = serializers.SerializerMethodField()
canDelete = serializers.SerializerMethodField()
interested = serializers.SerializerMethodField()
my_interest = serializers.SerializerMethodField()
jobs = serializers.SerializerMethodField()
partial=True
class Meta:
model = Event
fields = ['uuid','name','source','discord_event_id','my_interest','organization','short_name','content','owner','type','start','end','location','discord_link','discord_event_id','discord_description','discord_location','status','created','interested','jobs','canRead','canWrite','canEdit','canDelete']
def get_canRead(self,obj):
try:
if obj.owner == self.context['request'].user:
return True
except:
pass
if 'organization' in obj:
organization = obj.organization
else:
organization = None
return check_permission(self.context['request'].user, organization, 'event', 'read')
def get_canWrite(self, obj):
try:
if obj.owner == self.context['request'].user:
return True
except:
pass
if 'organization' in obj:
organization = obj.organization
else:
organization = None
return check_permission(self.context['request'].user, organization, 'event', 'write')
def get_canEdit(self, obj):
try:
if obj.owner == self.context['request'].user:
return True
except:
pass
if 'organization' in obj:
organization = obj.organization
else:
organization = None
return check_permission(self.context['request'].user, organization, 'event', 'edit')
def get_canDelete(self,obj):
try:
if obj.owner == self.context['request'].user:
return True
except:
pass
if 'organization' in obj:
organization = obj.organization
else:
organization = None
return check_permission(self.context['request'].user, organization, 'event', 'delete')
def get_jobs(self, obj):
try:
jobs = EventJob.objects.filter(event=obj, parent=None)
return EventJobSerializer(jobs, many=True, context={'request': self.context['request']}).data
except:
return None
def get_interested(self, obj, interested_list=None):
return UserStubSerializer(obj.interested, many=True).data if interested_list else None
def get_my_interest(self, obj):
try:
if self.context['request'].user.is_authenticated:
if self.context['request'].user in obj.interested.all():
return True
else:
return False
else:
return False
except:
return False
def create(self, validated_data):
validated_data['uuid'] = uuid.uuid4()
validated_data['owner'] = self.context['request'].user
event = Event.objects.create(**validated_data)
if event.uuid:
return event
else:
return None
I run the following test:
def test_event_edit_authenticated_non_org_user(self):
'''test that an event can be edited by an non-org user'''
#self.client.force_authenticate(user=self.user1)
response = self.client.post(
'/backend/api/event/',
{
'name': 'Test Event',
'content': 'This is a test event',
'start': '2020-01-01T00:00:00Z',
'end': '2020-01-01T01:00:00Z',
'organizationChange': self.org1.short_name,
'location': 'Test Location',
},
HTTP_AUTHORIZATION='Token ' + self.token1.key,
format='json'
)
self.assertEqual(response.status_code, 201)
event = Event.objects.get(uuid=response.data['uuid'])
#self.client.force_authenticate(user=self.user2)
response = self.client.patch(
'/backend/api/event/' + str(event.uuid) + '/',
{'uuid': str(event.uuid),
'name': 'Test Event 2',
'content': 'This is a test event',
'start': '2020-01-01T00:00:00Z',
'end': '2020-01-01T01:00:00Z',
'location': 'Test Location',
},
HTTP_AUTHORIZATION='Token ' + self.token2.key,
format='json'
)
self.assertEqual(response.status_code, 403)
I receive this error:
======================================================================
ERROR: test_event_edit_authenticated_non_org_user (events.tests.EventTest.test_event_edit_authenticated_non_org_user)
test that an event can be edited by an non-org user
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\env\scorg\django\events\tests.py", line 232, in test_event_edit_authenticated_non_org_user
event = Event.objects.get(uuid=response.data['uuid'])
~~~~~~~~~~~~~^^^^^^^^
KeyError: 'uuid'
I have a similar error in all of my tests which require a uuid as part of the response data.
Response for event creation: {'name': 'Test Event', 'source': None, 'discord_event_id': None, 'my_interest': False, 'organization': None, 'content': 'This is a test event', 'start': '2020-01-01T00:00:00Z', 'end': '2020-01-01T01:00:00Z', 'location': 'Test Location', 'discord_link': None, 'discord_description': None, 'discord_location': None, 'interested': None, 'jobs': None, 'canRead': True, 'canWrite': False, 'canEdit': False, 'canDelete': False}
This validated that the uuid field is not being returned.
lass EventSerializer(serializers.ModelSerializer):
...
class Meta:
model = Event
fields = ['uuid','name','source','discord_event_id','my_interest','organization','short_name','content','owner','type','start','end','location','discord_link','discord_event_id','discord_description','discord_location','status','created','interested','jobs','canRead','canWrite','canEdit','canDelete']
...
This validated that the 'uuid' field is included in the related serializer.
POST Request:
{
"name": "Test Event2",
"content": "This is a test event",
"start": "2020-01-01T00:00:00Z",
"end": "2020-01-01T01:00:00Z",
"organizationChange": "ESHORES",
"location": "Test Location"
}
Response:
{
"uuid": "7f7568b0-a07c-441d-915d-9ab3d3619a01",
"name": "Test Event2",
"source": "scorg",
"discord_event_id": null,
"my_interest": false,
"organization": {
"uuid": "6fcec5b4-b2b4-49ea-bcb4-93ff585670e4",
"name": "ESHORES",
"short_name": "ESHORES",
"url": "https://robertsspaceindustries.com/orgs/ESHORES/"
},
"short_name": "ESHORES",
"content": "This is a test event",
"owner": 231574499013820417,
"type": 0,
"start": "2020-01-01T00:00:00Z",
"end": "2020-01-01T01:00:00Z",
"location": "Test Location",
"discord_link": null,
"discord_description": null,
"discord_location": null,
"status": 0,
"created": "2023-01-11T20:07:02.783395Z",
"interested": null,
"jobs": [],
"canRead": true,
"canWrite": true,
"canEdit": true,
"canDelete": true
}
Clearly it correctly returns a uuid.
I expected that the test client post and the manual postman post would return the same fields, however, the test client does not. Does anyone have any idea why?
After walking through the logic, I discovered that there was an error in checking permissions. I was doing it too early and failed to include a parameter. I can't explain why it was working on postman but not on my tests, but changing the following corrected the error:
def perform_create(self, serializer):
user = self.request.user
org = None
# check if user has necessary permissions to create events
if 'organizationChange' in self.request.data:
try:
org = Organization.objects.get(short_name=self.request.data['organizationChange'])
except:
print('Organization does not exist')
pass
elif 'guild' in self.request.data:
try:
org = DiscordServer.objects.get(server_id=self.context['request'].data['guild']).org
except:
pass
perm = CheckPermission(capability='event', org=org, action='create').has_permission(self.request, self)
I moved the perm = CheckPermission() from above the 'if organizationChange' logic to below it, and that resolved it. The uuid wasn't coming through because the object was not successfully created.