Сериализация моделей SQLAlchemy для REST API при соблюдении контроля доступа? - PullRequest
7 голосов
/ 08 марта 2011

В настоящее время, как и наша, как и большинство веб-фреймворков, сериализация работает, есть некоторый тип вызова метода, который выгружает модель в некоторый тип формата. В нашем случае у нас есть метод to_dict() для каждой модели, который создает и возвращает словарь ключ-значение, где ключом является имя поля, а значением является переменная экземпляра.

Во всем нашем коде есть фрагменты, подобные следующему: json.dumps(**some_model_object.to_dict()), который сериализует some_model_object в json. Недавно мы решили предоставить некоторые внутренние ресурсы нашим пользователям, но некоторые из этих ресурсов имеют конкретные значения частных экземпляров, которые мы не хотим передавать обратно во время сериализации, если запрашивающий пользователь не является суперпользователем.

Я пытаюсь придумать чистый дизайн, который позволит упростить сериализацию, а также позволит сериализовать в формат, отличный от json. Я думаю, что это довольно хороший вариант использования для Аспектно-ориентированного проектирования / программирования, где аспекты учитывают запрашивающие элементы управления доступом и сериализуют объект на основе разрешений запрашивающего пользователя.

Вот что-то похожее на то, что у меня сейчас:

from framework import current_request


class User(SQLAlchemyDeclarativeModel):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    first_name = Column(Unicode(255))
    last_name = Column(Unicode(255))
    private_token = Column(Unicode(4096))

    def to_dict(self):
        serialized = dict((column_name, getattr(self, column_name))
                          for column_name in self.__table__.c.keys())

        # current request might not be bound yet, could be in a unit test etc.
        if current_request and not current_request.user.is_superuser():
            # we explicitly define the allowed items because if we accidentally add
            # a private variable to the User table, then it might be exposed.
            allowed = ['id', 'first_name', 'last_name']
            serialized = dict((k, v) for k, v in serialized.iteritems() if k in allowed)

        return serialized

Как видите, это далеко не идеально, потому что теперь мне нужно связать модель базы данных с текущим запросом. Хотя это очень явно, связь запросов - это запах кода, и я пытаюсь понять, как это сделать чисто.

Один из способов, который я задумал сделать, - это зарегистрировать некоторые поля в модели следующим образом:

class User(SQLAlchemyDeclarativeModel):
    __tablename__ = 'users'
    __public__ = ['id', 'first_name', 'last_name']
    __internal__ = User.__exposed__ + ['private_token']

    id = Column(Integer, primary_key=True)
    first_name = Column(Unicode(255))
    last_name = Column(Unicode(255))
    private_token = Column(Unicode(4096))

Тогда у меня будет класс сериализатора, связанный с текущим запросом при каждом вызове WSGI, который будет принимать нужный сериализатор. Например:

import simplejson

from framework import JSONSerializer  # json serialization strategy
from framework import serializer

# assume response format was requested as json
serializer.register_serializer(JSONSerializer(simplejson.dumps))
serializer.bind(current_request)

Тогда, на мой взгляд, я бы просто сделал:

from framework import Response

user = session.query(User).first()
return Response(code=200, serializer.serialize(user))

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

def serialize(self, db_model_obj):
    attributes = '__public__'
    if self.current_request.user.is_superuser():
        attributes = '__private__'

    payload = dict((c, getattr(db_model_obj, c)) 
                   for c in getattr(db_model_obj, attributes))

    return self.serialization_strategy.execute(payload)

Мысли о читаемости и ясности этого подхода? Это питонный подход к проблеме?

Заранее спасибо.

1 Ответ

7 голосов
/ 09 марта 2011

установить контракт "сериализации" через миксин:

class Serializer(object):
    __public__ = None
    "Must be implemented by implementors"

    __internal__ = None
    "Must be implemented by implementors"

    def to_serializable_dict(self):
        # do stuff with __public__, __internal__
        # ...

Упростите интеграцию с WSGI. "register", JSONSerializer как объект и все, что является чем-то вроде Java / Spring, не нуждается в такой фанфаре. Ниже мое решение в стиле пилонов 1.0, я еще не на пирамиде:

def my_controller(self):
   # ...

   return to_response(request, response, myobject)


# elsewhere
def to_response(req, resp, obj):
    # this would be more robust, look in
    # req, resp, catch key errors, whatever.
    # xxx_serialize are just functions.  don't need state
    serializer = {
       'application/json':json_serialize,
       'application/xml':xml_serialize,
       # ...
    }[req.headers['content-type']]

    return serializer(obj)
...