Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serializing SQLAlchemy models for a REST API while respecting access control?

Currently, the way our, as well as most web frameworks', serialization works is there's some type of method invocation which dumps the model into some type of format. In our case, we have a to_dict() method on every model that constructs and returns a key-value dictionary with the key being the field name and the value being the instance variable.

All throughout our code, we have snippets like the following: json.dumps(**some_model_object.to_dict()) which will serialize a some_model_object to json. Recently, we've decided to expose some internal resources to our users, but some of these resources have specific private instance values that we do not want to transmit back during serialization if the requesting user is not a super user.

I'm trying to come up with a clean design that will allow easier serialization, as well as allow us to serialize to a format other than json. I think this is a pretty good use case for Aspect Oriented Design/Programming, where the aspects respect the requesting access controls and serialize the object based on the requesting user's persmissions.

Here's something similar to what I have now:

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

As one can see, this is less than ideal because now I have to couple the database model with the current request. While this is very explicit, the request coupling is a code smell and I'm trying to see how to do this cleanly.

One way I've thought about doing it is to register some fields on the model like so:

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))

Then, I would have a serializer class that is bound with the current request on every WSGI call that will take the desired serializer. For example:

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)

Then in my view somewhere, I would just do:

from framework import Response

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

serialize would be implemented as follows:

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)

Thoughts on this approach's readability and clarity? Is this a pythonic approach to the problem?

Thanks in advance.

like image 909
Mahmoud Abdelkader Avatar asked Mar 08 '11 12:03

Mahmoud Abdelkader


1 Answers

establish the "serialization" contract via a mixin:

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__
        # ...

keep it simple with the WSGI integration. "register", JSONSerializer as an object, and all that is some kind of Java/Spring thing, don't need that fanfare. Below is my pylons 1.0-style solution, I'm not on pyramid yet:

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)
like image 91
zzzeek Avatar answered Oct 15 '22 08:10

zzzeek