Django ACL Design Question

124 views
Skip to first unread message

mguthrie

unread,
Apr 18, 2009, 1:08:14 PM4/18/09
to Django users
I've spent quite a bit of time investigating several frameworks for a
project I am starting. I initially wrote off Django due to it not
being able to handle my ACL needs. After reviewing other frameworks
I've determined that I will probably have to write something custom
regardless of the framework so I thought I would post my problem here
and see if anybody can point me in the right direction to get this
working in Django.

The requirements are as follows:

1.) A user can register with the site
2.) A user can belong to one or more organizations (by invite)
3.) Organizations cannot see each others' data.
4.) A user can have different permissions per organization. So, a
user could be an admin in one organization but only be able to view
records in another organization.

So basically the whole site pivots around an organization id.
Permissions need to be stored per organization not for the whole site
as the default acl currently works.

Think of something similar to a CRM where a person can belong to
different organizations that manage their own private records.

Any ideas on how to best integrate this with what Django already has
or be able to extend it without having to rewrite an Auth/ACL system
completely from scratch? If I have to write my own ACL is there a way
to integrate it with what Django has?

Thanks in advance for any help.

Tim Chase

unread,
Apr 18, 2009, 7:16:11 PM4/18/09
to django...@googlegroups.com
> Any ideas on how to best integrate this with what Django already has
> or be able to extend it without having to rewrite an Auth/ACL system
> completely from scratch? If I have to write my own ACL is there a way
> to integrate it with what Django has?

Well, Django makes it fairly easy to use your own auth and/or
permissions-verification. You may be able to get away with a
decorator. You'll have to create a richer permission model and
associate it with various views & users. You don't detail the
granularity of site-permissions (are they just
read/add/edit/delete, or are there more nuanced permissions; does
a user only have a single type of permission, or can they have a
mask of permissions? and are permission-types site-specific?):

PERM_READ, PERM_WRITE, PERM_DELETE, PERM_ADMIN = range(4)
SITE_PERMISSIONS = (
(PERM_READ, "Read"),
(PERM_WRITE, "Write"),
(PERM_DELETE, "Delete"),
(PERM_ADMIN, "Admin"),
)
class UserCompanies(models.Model):
user = models.ForeignKey(auth.User)
company = models.ForeignKey(Company)
permission = models.IntegerField(choices=SITE_PERMISSIONS)

Then create a decorator for your functions that determines the
permissions for the given user+company combo and asserts that
they are permitted:

def MustHavePermission(*required_perms):
def decorate(f):
def new_f(request, *args, **kwargs):
perms = UserCompanies.objects.filter(
user=request.user,
company=determine_company(request),
)
for perm in required_perms:
perms = perms.filter(permission=perm)
if not perms:
raise Http403("Sorry, Dave, I can't let you do that.")
return f(request, *args, **kwargs)
return new_f
return decorate

You should then be able to wrap your views in decorators like

@MustHavePermission(PERM_READ, PERM_WRITE)
def some_editing_view(request, param, otherparam):
pass

@MustHavePermission(PERM_READ, PERM_WRITE, PERM_DELETE):
def some_delete_view(request, id):
pass

@MustHavePermission(PERM_ADMIN)
def company_admin_view(request, *args, **kwargs):
pass

The above code is untested, but should be fairly close to the
actual implementation.

Things get a lot hairier if each company has their own set of
unique permissions instead of generic global permissions. I.e.
company A has "Frobniculate" and "Gerrywheeze" permissions, while
company B has "Porterloin" and "Munchagrunch" permissions. The
above code assumes that every company has read/write/delete/etc
permissions, and that they're just doled out on a per
user+company basis.

Hope this gives you some ideas on how to go about it.

-tim

Tim Chase

unread,
Apr 20, 2009, 6:35:40 AM4/20/09
to django...@googlegroups.com
> def MustHavePermission(*required_perms):
> def decorate(f):
> def new_f(request, *args, **kwargs):
> perms = UserCompanies.objects.filter(
> user=request.user,
> company=determine_company(request),
> )
> for perm in required_perms:
> perms = perms.filter(permission=perm)
> if not perms:
> raise Http403("Sorry, Dave, I can't let you do that.")
> return f(request, *args, **kwargs)
> return new_f
> return decorate

I noticed some serious bogosity in this logic (it only passed if
a single permission was required, never if multiple permissions
were required). That innermost function should be something like

def new_f(request, *args, **kwargs):
have = set(
UserCompanies.objects.filter(
user=request.user,
company=determine_company(request)
).values_list("permission", flat=True)
)
if have.issuperset(set(required_perms)):
return f(request, *args, **kwargs)
raise Http403("No way, Jose!")


-tim

Mitch Guthrie

unread,
Apr 20, 2009, 12:20:26 PM4/20/09
to django...@googlegroups.com
Tim,
   Thanks for the sample code.  It was exactly what I was looking for.  I'm still debating my route but it's nice to know what I want to do is possible.

Thanks.
Reply all
Reply to author
Forward
0 new messages