Skip to content

Commit

Permalink
Allow images to be posted with an api_key as well
Browse files Browse the repository at this point in the history
* Add tests for most common api key image cases
* Add data source field to image
* Refactor image permission validation to take place in LinkedEventsSerializer
  • Loading branch information
Rikuoja authored and juyrjola committed Dec 19, 2016
1 parent cfef656 commit d203c11
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 77 deletions.
136 changes: 66 additions & 70 deletions events/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,28 @@ def __init__(self, instance=None, files=None,
if 'disable_camelcase' in request.query_params:
self.disable_camelcase = True

# for post and put methods, user information is needed to restrict permissions at validate
if context is None:
return
self.method = self.context['request'].method
self.user = self.context['request'].user
if self.method in permissions.SAFE_METHODS:
return
# api_key takes precedence over user
if isinstance(self.context['request'].auth, ApiKeyAuth):
self.data_source = self.context['request'].auth.get_authenticated_data_source()
self.publisher = self.data_source.owner
if not self.publisher:
raise PermissionDenied(_("Data source doesn't belong to any organization"))
else:
# objects created by api are marked coming from the system data source unless api_key is provided
self.data_source = DataSource.objects.get(id=settings.SYSTEM_DATA_SOURCE_ID)
# user organization is used unless api_key is provided
self.publisher = self.user.get_default_organization()
if not self.publisher:
raise PermissionDenied(_("User doesn't belong to any organization"))


def to_internal_value(self, data):
for field in self.system_generated_fields:
if field in data:
Expand Down Expand Up @@ -436,6 +458,27 @@ def to_representation(self, obj):
return ret

def validate(self, data):
# validate data source permissions
if 'data_source' in data:
if data['data_source'] != self.data_source:
raise serializers.ValidationError(
{'data_source': _("Setting data_source to %(given)s " +
" is not allowed for your organization. The data source" +
" must be left blank or set to %(required)s") %
{'given': data['data_source'], 'required': self.data_source}})
else:
data['data_source'] = self.data_source
# validate publisher permissions
if 'publisher' in data:
if data['publisher'] != self.publisher:
raise serializers.ValidationError(
{'publisher': _("Setting publisher to %(given)s " +
" is not allowed for your organization. The publisher" +
" must be left blank or set to %(required)s ") %
{'given': data['publisher'], 'required': self.publisher}})
else:
data['publisher'] = self.publisher

if 'name' in self.translated_fields:
name_exists = False
languages = [x[0] for x in settings.LANGUAGES]
Expand All @@ -451,6 +494,11 @@ def validate(self, data):
return data

def create(self, validated_data):
# no django user exists for the api key
if isinstance(self.user, ApiKeyUser):
self.user = None
validated_data['created_by'] = self.user
validated_data['last_modified_by'] = self.user
try:
instance = super().create(validated_data)
except IntegrityError as error:
Expand All @@ -461,6 +509,17 @@ def create(self, validated_data):
return instance

def update(self, instance, validated_data):
if isinstance(self.user, ApiKeyUser):
# allow updating only if the api key matches instance data source
self.user = None
if not instance.data_source == self.data_source:
raise PermissionDenied()
else:
# without api key, the user will have to be admin
if not instance.is_user_editable() or not instance.is_admin(self.user):
raise PermissionDenied()
validated_data['last_modified_by'] = self.user

if 'id' in validated_data:
if instance.id != validated_data['id']:
raise serializers.ValidationError({'id':_("You may not change the id of an existing object.")})
Expand Down Expand Up @@ -748,25 +807,16 @@ def get_queryset(self):
queryset = queryset.filter(publisher__id=val)
return queryset

def perform_create(self, serializer):
user = self.request.user if not self.request.user.is_anonymous() else None
serializer.save(created_by=user, last_modified_by=user)

def perform_update(self, serializer):
# ensure image can only be edited within the organization
user = self.request.user
image = self.get_object()
if not user.get_default_organization() == image.publisher:
raise PermissionDenied()
user = self.request.user if not self.request.user.is_anonymous() else None
serializer.save(last_modified_by=user)

def perform_destroy(self, instance):
# ensure image can only be deleted within the organization
user = self.request.user
image = self.get_object()
if not user.get_default_organization() == image.publisher:
raise PermissionDenied()
auth = self.request.auth
if isinstance(auth, ApiKeyAuth):
if not auth.get_authenticated_data_source() == instance.data_source:
raise PermissionDenied()
else:
if not user.get_default_organization() == instance.publisher:
raise PermissionDenied()
super().perform_destroy(instance)


Expand Down Expand Up @@ -810,25 +860,6 @@ def __init__(self, *args, skip_empties=False, **kwargs):
# testing and debugging.
self.skip_empties = skip_empties

# for post and put methods, user information is needed to restrict permissions at validate
self.method = self.context['request'].method
self.user = self.context['request'].user
if self.method in permissions.SAFE_METHODS:
return
# api_key takes precedence over user
if isinstance(self.context['request'].auth, ApiKeyAuth):
self.data_source = self.context['request'].auth.get_authenticated_data_source()
self.publisher = self.data_source.owner
if not self.publisher:
raise PermissionDenied(_("Data source doesn't belong to any organization"))
else:
# events created by api are marked coming from the system data source unless api_key is provided
self.data_source = DataSource.objects.get(id=settings.SYSTEM_DATA_SOURCE_ID)
# user organization is used unless api_key is provided
self.publisher = self.user.get_default_organization()
if not self.publisher:
raise PermissionDenied(_("User doesn't belong to any organization"))

def get_datetimes(self, data):
for field in ['date_published', 'start_time', 'end_time']:
val = data.get(field, None)
Expand Down Expand Up @@ -864,26 +895,6 @@ def validate(self, data):
" is not allowed for your organization. The id"
" must be left blank or set to %(data_source)s:desired_id") %
{'given': str(data['id']), 'data_source': self.data_source}})
# validate data source permissions
if 'data_source' in data:
if data['data_source'] != self.data_source:
raise serializers.ValidationError(
{'data_source': _("Setting data_source to %(given)s " +
" is not allowed for your organization. The data source" +
" must be left blank or set to %(required)s") %
{'given': data['data_source'], 'required': self.data_source}})
else:
data['data_source'] = self.data_source
# validate publisher permissions
if 'publisher' in data:
if data['publisher'] != self.publisher:
raise serializers.ValidationError(
{'publisher': _("Setting publisher to %(given)s " +
" is not allowed for your organization. The publisher" +
" must be left blank or set to %(required)s ") %
{'given': data['publisher'], 'required': self.publisher}})
else:
data['publisher'] = self.publisher

# clean the html
for k, v in data.items():
Expand Down Expand Up @@ -972,9 +983,6 @@ def create(self, validated_data):
# if id was not provided, we generate it upon creation:
if 'id' not in validated_data:
validated_data['id'] = generate_id(self.data_source)
# no django user exists for the api key
if isinstance(self.user, ApiKeyUser):
self.user = None

offers = validated_data.pop('offers', [])
links = validated_data.pop('external_links', [])
Expand All @@ -995,21 +1003,9 @@ def create(self, validated_data):
return event

def update(self, instance, validated_data):
# allow updating events if the api key matches event data source
if isinstance(self.user, ApiKeyUser):
self.user = None
if not instance.data_source == self.data_source:
raise PermissionDenied()
else:
if not instance.is_editable() or not instance.is_admin(self.user):
raise PermissionDenied()

offers = validated_data.pop('offers', None)
links = validated_data.pop('external_links', None)

validated_data['last_modified_by'] = self.user


# The API only allows scheduling and cancelling events.
# POSTPONED and RESCHEDULED may not be set, but should be allowed in already set instances.
if validated_data.get('event_status') in (Event.Status.POSTPONED, Event.Status.RESCHEDULED):
Expand Down
2 changes: 1 addition & 1 deletion events/importer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def get_or_create_image(self, url):
if url in self._images:
return self._images[url]

defaults = {'publisher': self.organization}
defaults = {'publisher': self.organization, 'data_source': self.data_source}
img, created = Image.objects.get_or_create(
url=url, defaults=defaults)
return img
Expand Down
31 changes: 31 additions & 0 deletions events/migrations/0033_add_data_source_to_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2016-11-28 14:24
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion



def forwards_func(apps, schema_editor):
Image = apps.get_model('events', 'Image')

for image in Image.objects.filter(event__isnull=False).distinct():
image.data_source = image.event_set.all()[0].data_source
image.save()


class Migration(migrations.Migration):

dependencies = [
('events', '0032_add_super_event_type'),
]

operations = [
migrations.AddField(
model_name='image',
name='data_source',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='provided_image_data', to='events.DataSource'),
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
]
13 changes: 12 additions & 1 deletion events/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class Image(models.Model):
# Properties from schema.org/Thing
name = models.CharField(verbose_name=_('Name'), max_length=255, db_index=True, default='')

data_source = models.ForeignKey(DataSource, related_name='provided_%(class)s_data', db_index=True, null=True)
publisher = models.ForeignKey('Organization', verbose_name=_('Publisher'), db_index=True, null=True, blank=True, related_name='Published_images')

created_time = models.DateTimeField(auto_now_add=True)
Expand Down Expand Up @@ -129,6 +130,16 @@ def save(self, *args, **kwargs):
self.last_modified_time = BaseModel.now()
super(Image, self).save(*args, **kwargs)

def is_user_editable(self):
return self.data_source_id == settings.SYSTEM_DATA_SOURCE_ID

def is_admin(self, user):
if user.is_superuser:
return True
else:
return user in self.publisher.admin_users.all()


@python_2_unicode_compatible
class BaseModel(models.Model):
id = models.CharField(max_length=50, primary_key=True)
Expand Down Expand Up @@ -420,7 +431,7 @@ def __str__(self):
val.append(str(self.start_time))
return u" ".join(val)

def is_editable(self):
def is_user_editable(self):
return self.data_source_id == settings.SYSTEM_DATA_SOURCE_ID

def is_admin(self, user):
Expand Down
Loading

0 comments on commit d203c11

Please sign in to comment.