Enforcing data isolation with CurrentDomainProperty

In a previous post, we described how to implement API call hooks, and demonstrated a common use-case: Separating the datastore by domain, for multi-tenant apps.

It's not always the case that you want to partition your entire datastore along domain or user lines, however. Sometimes you may want to have only some models with restricted access per-domain, with others being common across all domains. You might also want a way to ensure that users can't read or modify each others' data. Fortunately, there's a way to implement all this at a higher level: Instead of defining API call hooks, we can define custom datastore properties to do the job for us.

Here's an implementation of a CurrentDomainProperty:

class InvalidDomainError(Exception):
  """Raised when something attempts to access data belonging to another domain."""


class CurrentDomainProperty(db.Property):
  """A property that restricts access to the current domain."""

  def __init__(self, allow_read=False, allow_write=False, *args, **kwargs):
    self.allow_read = allow_read
    self.allow_write = allow_write
    super(CurrentDomainProperty, self).__init__(*args, **kwargs)

  def __set__(self, model_instance, value):
    if not value:
      value = unicode(os.environ['HTTP_HOST'])
    elif (value != os.environ['HTTP_HOST'] and not self.allow_read
          and not users.is_current_user_admin()):
      raise InvalidDomainError(
          "Domain '%s' attempting to illegally access data for domain '%s'"
          % (os.environ['HTTP_HOST'], value))
    super(CurrentDomainProperty, self).__set__(model_instance, value)

  def get_value_for_datastore(self, model_instance):
    value = super(CurrentDomainProperty, self).get_value_for_datastore(
        model_instance)
    if (value != os.environ['HTTP_HOST'] and not users.is_current_user_admin()
        and not self.allow_write):
      raise InvalidDomainError(
          "Domain '%s' attempting to allegally modify data for domain '%s'"
          % (os.environ['HTTP_HOST'], value))
    return value

This should be fairly easy to follow if you've read the previous article on custom Datastore properties. We define an 'allow_read' property in the constructor to permit the creation of models containing data that should be readable by other domains, but not writable, and an 'allow_write' property to permit the creation of models that want the domain automatically set, but don't want to restrict access.

The way we override __set__ is necessary because of the way values are assigned to property classes: On first instantiation with no default provided, __set__ will be called by the db module with None, in which case we set the value to the current domain. Subsequently loading the entity from the datastore causes __set__ to be called with a value, which we check against the current domain, throwing an InvalidDomainError exception if it's not the expected value and read access isn't permitted. We also give admin users a free pass to read and modify anything.

We don't need to override __get__, but we do need to override get_value_for_datastore. Doing so allows us to detect when an entity is being written back to the datastore, and throw an exception if we shouldn't permit this. Again, we check if the domain matches, or the user is an admin, or writing is permitted for this property.

Here's an example of our CurrentDomainProperty in action:

>>> class DomainModel(db.Model):
...   domain = CurrentDomainProperty()

>>> os.environ['HTTP_HOST'] = 'domain1'
>>> model = DomainModel()
>>> key = model.put()
>>> model = DomainModel.get(key)
>>> model.domain
u'domain1'

# You cannot read or write the data from another domain:

>>> os.environ['HTTP_HOST'] = 'domain2'
>>> model.put()
Traceback (most recent call last):
    ...
InvalidDomainError: Domain 'domain2' attempting to allegally modify data for domain 'domain1'

You can see the complete CurrentDomainProperty here in the latest version of aetycoon.

A CurrentUserProperty would be another useful property, applying the same restrictions to access by logged in users as the CurrentDomainProperty provides for domains. Implementing such a property is left as an exercise to the reader.

Comments

blog comments powered by Disqus