Проблемы с ForeignKey при использовании POST в Django-Tastypie

Я создаю простой API, используя django-tastypie. Идея в том, что у меня есть два ресурса:

  • Ресурс Note, представляющий заметку, оставленную пользователем. Только пользователь, создавший заметку, может редактировать ее.
  • Ресурс Комментарий. Комментарии к любой заметке может оставлять любой пользователь.

TL;DR: я не могу ограничить редактирование заметки создателем заметки, но при этом разрешить любому пользователю комментировать заметку.

Я использую следующую настройку для аутентификации:

class CreatedByEditAuthorization(Authorization):
    def is_authorized(self, request, object=None, **kwargs):
        return True

    def apply_limits(self, request, object_list):
        if request and request.method != 'GET' and hasattr(request, 'user'):
            return object_list.filter(created_by=request.user)
        return object_list

Короче говоря, пользователю разрешено редактировать только те объекты, для которых он равен свойству created_by (он может редактировать только созданные им объекты).

Это связано следующим образом:

class NoteResource(ModelResource):
    comments = fields.ToManyField('myapp.api.resources.CommentResource', 'comments', null=True, blank=True)
    created_by = fields.ToOneField('account.api.resources.UserResource', 'created_by')

    def obj_create(self, bundle, request, **kwargs):
        return super(HapResource, self).obj_create(bundle, request, created_by=request.user)

    class Meta:
        queryset = Note.objects.all()
        allowed_methods = ['get', 'put', 'post']
        authorization = CreatedByEditAuthorization()

поэтому здесь, когда создается объект, я автоматически привязываю текущего пользователя к атрибуту created_by и привязываю его к соответствующей авторизации.

Ресурс Comment прост и имеет только ресурсы от ForeignKey до Note.

Проблема заключается в следующем: если пользователь А создает заметку, а пользователь Б пытается прокомментировать эту заметку, deliciouspie отправляет (или имитирует) запрос POST для редактирования этой заметки. Эта попытка отклонена, так как пользователь Б не создал примечание, поэтому создать комментарий не удается.

Вопрос в следующем: есть ли способ:

  1. Предотвратить использование deliciouspie POST для создания обратной связи с ресурсом Note или
  2. Изменить схему авторизации, чтобы заметки мог редактировать только их создатель, а комментарии можно было создавать вообще?

Заранее спасибо за любую информацию.

Редактировать: у меня есть большой лайфхак, который может это сделать. Я вполне уверен, что это безопасно, но я не уверен; Я попробую построить несколько запросов, чтобы убедиться. Вместо того, чтобы использовать fields.ForeignKey в Comment для связи с Note, я создаю настраиваемое поле:

class SafeForeignKey(fields.ForeignKey):
    def build_related_resource(self, value, request=None, related_obj=None, related_name=None):
        temp = request.method
        if isinstance(value, basestring):
            request.method = 'GET'
        ret = super(SafeForeignKey, self).build_related_resource(value, request, related_obj, related_name)
        request.method = temp
        return ret

Каждый раз, когда мы пытаемся создать этот связанный ресурс, мы помечаем запрос как GET (поскольку мы ожидаем, что он будет соответствовать запросу SELECT, а не UPDATE, который соответствует PUT или POST). Это действительно уродливо и потенциально небезопасно при неправильном использовании, и я надеюсь на лучшее решение.

Редактировать 2: Насколько я могу судить из исходного кода deliciouspie, нет способа отфильтровать авторизацию по запросу, который фактически будет отправлен.


person Alex Churchill    schedule 26.12.2011    source источник
comment
Пара вопросов. Используете ли вы contrib.comments? Используете ли вы аутентификацию, а также авторизацию? У меня есть то, что кажется очень похожей настройкой (без похожего CommentResource), которая отлично работает при публикации нового комментария к другому объекту пользователя.   -  person JamesO    schedule 13.01.2012
comment
@JamesO Нет, наши комментарии несколько богаче, чем предоставляет contrib.comments (и есть другие данные, связанные с публикацией, имеющей ту же проблему). В настоящее время мы просто используем встроенную аутентификацию() (т.е. все аутентифицируются).   -  person Alex Churchill    schedule 14.01.2012
comment
Вы разместили это как проблему на django-tastypie: github.com/toastdriven/django-tastypie/ проблемы ? Если он действительно пытается обновить родительскую запись каждый раз, когда вы создаете что-то, связанное с ней, это скорее ошибка, чем функция.   -  person Jordan Reiter    schedule 16.01.2012
comment
@JordanReiter см. github.com/toastdriven/django-tastypie /blob/master/tastypie/ — кажется, что deliciouspie сохраняет все связанные поля, и есть даже комментарий, что он делает это «на всякий случай».   -  person gingerlime    schedule 17.01.2012
comment
@Yoav, знаем ли мы, является ли save_related когда-либо необходимым вызовом?   -  person Alex Churchill    schedule 17.01.2012
comment
Я не уверен. Можете спросить у разработчиков deliciouspie. Однако я не думаю, что ваша проблема заключается не в save_related, а скорее в предыдущем вызове full_hydra (где в конечном итоге создается связанный ресурс Notes и вызывается его функция apply_limits, которая ограничивает ваши результаты текущим пользователем)   -  person gingerlime    schedule 18.01.2012


Ответы (2)


Согласно обсуждению на https://github.com/toastdriven/django-tastypie/issues/480#issuecomment-5561036:

Метод, который определяет, можно ли обновить Resource, называется can_update. Следовательно, чтобы это работало «правильным» образом, вам нужно создать подкласс NoteResource:

class SafeNoteResource(NoteResource):
    def can_update(self):
        return False
    class Meta:
        queryset = Note.objects.all()
        allowed_methods = ['get']
        authorization = Authorization()
        # You MUST set this to the same resource_name as NoteResource
        resource_name = 'note'

затем пусть CommentResource ссылается на заметки стандартным способом: note = fields.ForeignKey(SafeNoteResource, 'note').

person Alex Churchill    schedule 07.05.2012

простым решением должна быть проверка внутри apply_limits, относится ли запрос к ресурсу Note или ресурсу Comment. например что-то типа

def apply_limits(self, request, object_list):
    if request and request.method != 'GET' and hasattr(request, 'user') and getattr(request, 'path','').startswith('/api/v1/note'):
        return object_list.filter(created_by=request.user)
    return object_list

Затем вы ограничиваете доступ к Notes только для одного и того же пользователя, когда пользователь обращается к ресурсу Note напрямую, а не через другие связанные ресурсы, такие как Комментарии.

update: или немного более безопасным вариантом будет проверка того, что запрос не начинается с 'api/v1/comment', поэтому вы просто вносите в белый список доступ к комментариям, а не что-либо< /em> кроме примечания. Однако применяется тот же принцип. Будьте осторожны с этим текстовым сравнением пути запроса, чтобы избежать случаев, когда кто-то просто добавляет/добавляет строку к вашему URL-адресу, чтобы обойти вашу авторизацию. Надеюсь, добавление в начало более ограничено, поскольку ему нужно указать правильный URL-адрес в urls.py, поэтому я использовал здесь startswith. Конечно, вам придется настроить строку пути, чтобы она соответствовала URL-адресам вашего вкусного пирога.

person gingerlime    schedule 16.01.2012
comment
Это кажется большим взломом, чем то, что я предложил; разрешения на уровне таблицы должны обрабатываться в Meta для корреспондента Resource. Кроме того, как вы сказали, сравнение на основе текста может быть опасным (и на самом деле также может вызвать ошибки, если, скажем, вы должны были иметь отношение к комментарию с full=True - в этом случае URL-адрес может не включать /comment /). Что касается того, что возвращается из пакета, вы правы в том, что путь — это действительно единственные важные данные, которые у вас есть. - person Alex Churchill; 17.01.2012
comment
не уверен, что мое предложение является большим взломом, чем изменение метода запроса с POST на GET (не говоря уже о добавленных строках кода). В предложенном мной решении авторизация обрабатывается в правильном месте (это apply_limits), и проверка URL-адреса может выполняться независимо от того, full=True или нет. Мы проверяем URL-адрес запроса, а не какие-либо данные пакета. Возможно, мое решение можно немного улучшить, добавив какой-нибудь флаг к объекту запроса внутри ресурса комментариев obj_create, а затем проверив его в методе apply_limits Notes. - person gingerlime; 18.01.2012
comment
Это большой хак, потому что он нарушает модульность. Авторизация на уровне таблицы должна быть реализована на уровне ресурса, а не встроена в схему аутентификации. В приведенном выше хаке вы меняете метод запроса, но только при следовании отношению к Note из Comment, поэтому вы сохраняете изменение схемы локализованным для данного ресурса. Прежде чем опубликовать хак POST-GET, я подумал об использовании пути (на самом деле более надежным/последовательным способом, изменив способ построения путей), но отказался от этого, потому что это нарушило модульность. - person Alex Churchill; 19.01.2012
comment
Похоже, вы путаете аутентификацию и авторизацию. Мое предложение было встроено в класс авторизации ресурса Notes, поэтому я не уверен, почему вы называете его «схемой аутентификации» или думаете, что это нарушает модульность. Это как раз то место, где вкусняшка предназначена для проверки авторизации и ограничения/разрешения доступа. Кстати, если вы беспокоитесь о безопасности, вы не опубликовали весь код, но похоже, что исходный код, который вы опубликовали, уже может быть уязвим в нескольких местах. В любом случае удачи. - person gingerlime; 19.01.2012
comment
Извините, аутентификация должна говорить авторизация. Это нарушает модульность, потому что вы не должны писать собственный класс авторизации для каждого ресурса. Вам следует выбрать правильный существующий класс авторизации. Я больше сочувствую пользовательскому переопределению apply_limits на уровне ресурсов, но это все еще имеет свою долю проблем. С точки зрения уязвимости безопасности, это, безусловно, проблема, но это также проблема с сопоставлением строк. В обоих случаях правильная реализация будет безопасной, но ошибка программиста в дальнейшем может поставить систему под угрозу. - person Alex Churchill; 19.01.2012
comment
Я лично считаю, что имеет смысл предоставлять собственные классы авторизации для ваших ресурсов. Авторизация, если она выполнена правильно, в любом случае, скорее всего, будет привязана к конкретному ресурсу. На уровне ресурсов нет apply_limits, но вы можете переопределить apply_authorization_limits. Как я уже упоминал, вам не нужно выполнять сопоставление строк. Вы можете добавить флаг к запросу. А что касается безопасности - я считаю, что ваш текущий код может уже быть небезопасным в нескольких местах. Хотя это не имеет прямого отношения к вашему вопросу. - person gingerlime; 20.01.2012
comment
Этим утром я довольно подробно прочитал исходный код и сделал небольшую модификацию (см. выше) — теперь я меняю на GET только в том случае, если я гарантирую, что он создает связанный ресурс через URI. Другими словами, я могу гарантировать, что мой взлом не откроет никаких уязвимостей в системе безопасности. Я нашел две уязвимости в вашем взломе. Во-первых, вы можете гидратировать из связанного_ресурса — в этом случае вы можете создать/изменить Note, даже если ваш базовый вызов был comment. Во-вторых, если вы изменяете параметр запроса, но имеете несколько связанных ресурсов, он изменяется для каждого из них, что вызывает проблемы. - person Alex Churchill; 20.01.2012