10

django guardian https://github.com/lukaszb/django-guardian is a really well written object-level permissions app; and I have actually read up on and used quite a number of other django object level permissions app in various django projects.

In a recent project that I am working on, I decided to use django guardian but I have a model design question relating to the pros and cons of two possible approaches and their respective implications on sql query performance:-

  1. using django.contrib.auth.models.Group and extending that to my custom organization app's models; or

  2. using django.contrib.auth.models.User instead and creating an m2m field for each of the organization type in my organization app.

Approach #1

# Organisation app's models.py

from django.contrib.auth.models import Group

class StudentClass(models.Model):
    name = models.CharField('Class Name', max_length=255)
    groups = models.ManyToManyField(Group, blank=True)
    size = models.IntegerField('Class Size', blank=True)

class SpecialInterestGroup(models.Model):
    name = models.CharField('Interest Group Name', max_length=255)
    groups = models.ManyToManyField(Group, blank=True)
    description = models.TextField('What our group does!', blank=True)

class TeachingTeam(models.Model):
    name = models.CharField('Teacher Team Name', max_length=255)
    groups = models.ManyToManyField(Group, blank=True)
    specialization = models.TextField('Specialty subject matter', blank=True)

In this approach, when a user is added to a group (django group) for the first time, the group object is created and also assigned to one of these 3 classes, if that group object does not yet belong to the class it is added into.

This means that each StudentClass object, sc_A, sc_B etc, can possibly contain a lot of groups.

What that means is that for me to ascertain whether or not a specific user (say myuser) belongs to a particular organization, I have to query for all the groups that the user belong to, via groups_myuser_belongto = myuser.groups and then query for all the groups that are associated to the organization I am interested in, via groups_studentclass = sc_A.groups.all() and since I now have 2 lists that I need to compare, I can do set(groups_myuser_belongto) && set(groups_studentclass), which will return a new set which may contain 1 or more groups that intersect. If there are 1 or more groups, myuser is indeed a member of sc_A.

This model design therefore implies that I have to go through a lot of trouble (and extra queries) just to find out if a user belongs to an organization.

And the reason why I am using m2m to groups is so as to make use of the Group level permissions functionality that django guardian provides for.

Is such a model design practical?

Or am I better off going with a different model design like that...

Approach #2

# Organisation app's models.py

from django.contrib.auth.models import User

class StudentClass(models.Model):
    name = models.CharField('Class Name', max_length=255)
    users = models.ManyToManyField(User, blank=True)
    size = models.IntegerField('Class Size', blank=True)

class SpecialInterestGroup(models.Model):
    name = models.CharField('Interest Group Name', max_length=255)
    users = models.ManyToManyField(User, blank=True)
    description = models.TextField('What our group does!', blank=True)

class TeachingTeam(models.Model):
    name = models.CharField('Teacher Team Name', max_length=255)
    users = models.ManyToManyField(User, blank=True)
    specialization = models.TextField('Specialty subject matter', blank=True)

Obviously, this model design makes it really easy for me to check if a user object belongs to a particular organization or not. All I need to do to find out if user john is part of a TeachingTeam maths_teachers or not is to check:

user = User.objects.get(username='john')
maths_teachers = TeachingTeam.objects.get(name='Maths teachers')
if user in maths_teachers.users.all():
    print "Yes, this user is in the Maths teachers organization!"

But what this model design implies is that when I add a user object to a group (recall that I want to use django guardian's Group permissions functionality), I have to make sure that the save() call adds the user object into a "Maths Teachers" group in django.contrib.auth.models.Group AND into my custom TeachingTeam class's "Maths Teachers" object. And that doesn't feel very DRY, not to mention that I have to somehow ensure that the save calls into both the models are done in a single transaction.

Is there a better way to design my models given this use case/requirement - use django groups and yet provide a way to "extend" the django's native group functionality (almost like how we extend django's user model with a "user profile app")?

Calvin Cheng
  • 35,640
  • 39
  • 116
  • 167
  • Why don't you use `OneToOne` while linking your objects with groups? – ilvar Apr 23 '12 at 04:17
  • Because unlike User-to-Userprofile relationship (which uses either `OneToOne` or `ForeignKey`, because 1 userprofile is conceivably only related to 1 user), it is possible that an 'organization' class can be associated with many groups. – Calvin Cheng Apr 23 '12 at 04:23
  • So what do Django groups mean? I thought your groups should inherit Django's groups just as user profiles do (i meant first approach, of course) – ilvar Apr 24 '12 at 03:53
  • django groups could mean different groups in a particular type of organization. For example, a `StudentClass` can comprise of multiple project groups. – Calvin Cheng Apr 24 '12 at 05:20

2 Answers2

9

My take on this (having developed django apps for a long time) is that you should stick with the natural approach (so a StudentClass has Users rather than Groups). Here "natural" means that it correspond to the actual semantics of the involved objects.

If belonging to a specific StudentClass must imply some automatic group (in addition to those granted to the user), add a groups m2m to the StudentClass model, and create a new authentication backend (extending the default one), which provides a custom get_all_permissions(self, user_obj, obj=None) method. It will be hooked by https://github.com/django/django/blob/master/django/contrib/auth/models.py#L201

In this method query for any group associated to any Organization the user belongs to. And you don't need to do 1+N queries, correct use of the ORM will navigate through two *-to-many at once.

The current ModelBackend method in https://github.com/django/django/blob/master/django/contrib/auth/backends.py#L37 queries get_group_permissions(user_obj) and adds them to the perms the user has assigned. You could add similar behavior by adding (cached) get_student_class_permission and other corresponding methods.

(edited for clearer prologue)

rewritten
  • 16,280
  • 2
  • 47
  • 50
  • yes, group concept are making things more complex if you do not need that kind of granularity control – zinking Jun 26 '12 at 02:49
  • This is super helpful. Make sure you either inherit from ModelBackend or also add methods for `get_perm` and `get_all_permissions`, even if they just call `get_group_permissions`. – jgiuffrida Oct 14 '17 at 23:30
0

Obs: There is another approach which is to use generic relationships, in this approach you would have the User model instance pointing to it's resources through the contenttypes framework. There is a nice question here on SE explaining this approach.

About the performance: There are some clues that a single select with JOIN is cheaper than many simple select (1,2,3). In this case opition 2 would be better.

About the usability: The first approach is hard to explain, hard to understand. IMHO go for no. 2. Or try the generic relations.

Community
  • 1
  • 1
Pablo
  • 410
  • 5
  • 12