views:

269

answers:

3

To sum things up before I get into bad examples, et al: I'm trying to make an application where I don't have to write code in all my models to limit choices to the current logged in account (I'm not using Auth, or builtin features for the account or login).

ie, I don't want to have to do something like this:

class Ticket(models.Model):
        account = models.ForeignKey(Account)
        client = models.ForeignKey(Client)  # A client will be owned by one account.
        content = models.CharField(max_length=255)

class TicketForm(forms.ModelForm):
        class Meta:
                model = Ticket
                exclude = ('account',)  #First sign of bad design?

        def __init__(self, *args, **kwargs):
                super(OrderForm, self).__init__(*args, **kwargs)
                if self.initial.get('account'):
                        # Here's where it gets ugly IMHO. This seems almost
                        # as bad as hard coding data. It's not DRY either.
                        self.fields['client'].queryset = Client.objects.filter(account=self.initial.get('account'))

My idea is to create an Account(models.Model) model with the following custom manager, and subclass it using multi-table inheritance with all of my models. It's giving me a huge brain ache though. Will I still need an account foreign key on each model? Can I access the parent class account for a certain model instance?

class TicketManager(models.Manager):
    def get_query_set(self):
        return super(TicketManager, self).get_query_set().filter(account=Account.objects.get(id=1))
        # Obviously I don't want to hard code the account like this.
        # I want to do something like this:
        # return super(ProductManager, self).get_query_set().filter(account=self.account)
        # Self being the current model that's using this manager
        # (obviously this is wrong because you're not inside a model
        # instance , but this is where the confusion comes in for me.
        # How would I do this?).

Please ignore any blaring syntax errors. I typed this whole thing in here.

Here's where I got the idea to do this: Django Namespace project

+1  A: 

The basic problem here is, even though you don't use django.contrib.auth that information about the current logged in user is only available in the view and never in the model, because this information is bound to a request. So you will always have to do something like this in the view:

def some_view(request):
    account = get_account_by_request(request)

Then you can use the account to filter models. You can always make this approach more elegant by using a middleware or a decorator but beware that it doesn't get too tricky. Your design might break at unexpected points (happened to me) because of too much multiple inheritances with inherited managers and the like. Keep it simple and predictable.

stefanw
That's kind of what I've done in the past, but I'm wondering if there's some middleware I can design, or something that can initialize the account from the model and request. I could build a customer manager method that takes the request.
orokusaki
You could do something like Account.special_objects(request).filter..., but be aware: you are mixing the view layer with the model layer. You should keep things DRY, but you should also keep them decoupled.
stefanw
Yea, that's why this is such a brain fart for me. I just don't want to have to micro manage object access for each account, and I'm having the toughest time figuring this out. Micromanaging could lead (especially with me) to a coding mistake that allows one account admin to access another account holder's objects.
orokusaki
+1  A: 

In order to get current user into your code, you can use threadlocals middleware but there isn't anything fancy about that, just one big hack, so watch out :)

Personally, i think it's bad to put that kind of logic into models, because they're just data and should not be aware of such things.

Dmitry Shevchenko
Yea, to be honest, I steer clear of anything to do with any threading library unless it's needed for serious multithreading or multiprocessing. I have OCD so I can't stand hacks and tricks (which is why everything takes me 3x as long :) Thanks for your answer though.
orokusaki
+5  A: 

There are two closely related problems when it comes to Django.

One is row level permissions where users/accounts need specific permission to view a specific row (object) in a table, as opposed to normal Django auth framework which has table level permissions.

The project you linked to is one of several projects trying to implement row permissions. django-granular-permissions is another and a third (my favorite and the most active/maintained) is django-authority.

The upcoming release of Django 1.2 will have hooks making row-level permissions easier to implement, and the author of django-authority will work on integrating his project.

The second related problem is something called multi-tenant database which is a variation on row permissions. In this scheme you might have multiple users from a single company, for example, who all have access to data for that company but not other companies (tenants).

I don't think this is what you're looking for but you might be able to use some of the same techniques. See how to enforce account separation in Django and multi-tenant django applications. Both have really sparse answers but are a starting point as well as looking at multi-tenant architecture for Rails apps and this article.

As for a more specific answer to your question I think you should either use django-authority or write a custom manager and use the record ownership screener during development to verify your queries aren't bypassing the custom manager.

Van Gale
Perfect, you understand what I'm doing perfectly. Thanks for taking the time to put all that information. I'm developing a SAAS which will have multiple accounts (hopefully), and each will have multiple users. The users isn't such a big problem because I'll be writing some basic @accountowneronly type decorators at the view level so that account admins will have to be an account owner or my own variant of a superuser to access certain views. The huge issue for me is making an efficient and extensible architecture so that I can almost code as if I was developing this for one account only.
orokusaki
Ah nice. Well just as an additional note the (probably) ideal solution for us in the long term is support for Postgresql schemas which adds this protection at the database level. See http://code.djangoproject.com/ticket/1051 and http://code.djangoproject.com/ticket/6148 (this is "medium priority" so will not be in django 1.2 see http://code.djangoproject.com/wiki/Version1.2Features)
Van Gale
Also see http://stackoverflow.com/questions/1160598/how-to-use-schemas-in-django
Van Gale
Thanks Van. You've been really helpful. I didn't realize how much of a nightmare multiple tenants really is until these past few days.
orokusaki