views:

235

answers:

1

Hello.

I'm trying to implement row-based security checks for Django models. The idea is that when I access model manager I specify some additional info which is used in database queries so that only allowed instances are fetched from database.

For example, we can have two models: Users and, say, Items. Each Item belongs to some User and User may be connected to many Items. And let there be some restrictions, according to which a user may see or may not see Items of another User. I want to separate this restrictions from other query elements and write something like:

items = Item.scoped.forceRule('user1').all()  # all items visible for 'user1'

or

# show all items of 'user2' visible by 'user1'
items = Item.scoped.forceRule('user1').filter(author__username__exact = 'user2')

To acheive this I made following:

class SecurityManager(models.Manager):

    def forceRule(self, onBehalf) :
        modelSecurityScope = getattr(self.model, 'securityScope', None)
        if modelSecurityScope :
            return super(SecurityManager, self).get_query_set().filter(self.model.securityScope(onBehalf))
        else :
            return super(SecurityManager, self).get_query_set()

    def get_query_set(self) :
        #
        # I need to know that 'onBehalf' parameter here
        #  
        return super(SecurityManager, self).get_query_set()

class User(models.Model) :
    username = models.CharField(max_length=32, unique=True)

class Item(models.Model) :
    author = models.ForeignKey(User)
    private = models.BooleanField()
    name = models.CharField(max_length=32)

    scoped = SecurityManager()

    @staticmethod
    def securityScope(onBehalf) :
        return Q(author__username__exact = onBehalf) | Q(bookmark__private__exact = False)

For shown examples it works fine, but dies on following:

items = Item.scoped.forceRule('user1').filter(author__username__exact = 'user2') # (*)
items2 = items[0].author.item_set.all()  # (**)

Certainly, items2 is populated by all items of 'user2', not only those which conform the rule. That is because when all() is executed SecurityManager.get_query_set() has no information about the restriction set. Though it could. For example, in forceRule() I could add a field for every instance and then, if I could access that field from manager, apply the rule needed.

So, the question is - is there any way to pass an argument provided to forceRule() in statement (*) to manager, called in statement (**).

Or another question - am I doing strange things that I shouldn't do at all?

Thank you.

+1  A: 

From my reading of the documentation I think there are two problems:

  1. The SecurityManager will not be used for the related objects (and instance of django.db.models.Manager will be used instead)
  2. You can fix the above, but the documentation goes to great lengths to specify that get_query_set() should not filter out any rows for related queries.

I suggest creating a function that takes a QuerySet and applies the filter you need to it. This can then be used whenever you get to a QS of Items and want to process them further.

Colin Stewart
Thank you for the answer.1. I read the documentation and was very suprised when during `items[0].author.item_set.all()` Django called SecurityManager.get_query_set(). I didn't ask them to with use_for_related_fields. But it was the only manager for the model, maybe it matters.2. Yes, I read it but still think it is not my case, because I really want Django to see not all the database but part of it.Probably, I'll do somehow like you suggested, but my question is not how to filter out irrelevant data, but to make this filtering as easy and as automatic as possible.