4

we are running multiple django sites (let's call them site1, site2, site3) against the same database, and we'd like to permit duplicated usernames accross them. Site and auth framework do not seem to achieve this, by default, username is a unique field in auth.User.

So what I've done so far (monkey patch, messing up with the user object...):

User._meta.get_field('username')._unique = False
User.add_to_class('site', models.ForeignKey(Site, default=Site.objects.get_current().id, blank=True, null=True))
User._meta.unique_together = (('username', 'site'),)

This piece removes the uniqueness of username, add a site field, make the couple (username, site) unique.

Then come problems which could occure when requesting a User.objects.get(username=xx) (e.g., authentication backends), if some users have the same username on different site. So, I decided to patch the User.objects manager:

def get_query_set(filter=True):
    q = QuerySet(User.objects.model, using=User.objects._db)
    if filter:
        return q.filter(site = Site.objects.get_current())
    return q
User.objects.get_query_set = get_query_set

Seems to work so far. But... the sites use pretty much the same objects, and it's all likely we change user field of these objects using the admin interface, which is common to all sites... hence, if I want to attribute an object (which has a foreignkey to auh.User) to a user of site2 while being logged in as admin on site1, that won't work, as the user manager will filter on site=site1.

I digged up a little, found that this seems to work:

class UserDefaultManager(UserManager):
    def get_query_set(self, filter=None):
        return QuerySet(User.objects.model)
User._default_manager = UserDefaultManager()

As far as I understand, _default_manager is used by the related objects manager. Then, User.objects.get(username=xx) filter on sites, and an_object.user won't.

Well, question is: yes, this is messy, and I'm pretty sure there will be flaws, but which are they ?

Next question is: if it's valid, then where is the best place to put this piece of code ? It's currently in a models.py file, just ran as the module is loaded...

fylb
  • 679
  • 4
  • 10
  • Would it not be simpler to store which sites a username is valid for and check against this list during authentication? This way you could offer a user starting on a different site that already uses one the ability to reuse their login, much like Stack Exchange does. – Vic Smith Aug 29 '12 at 09:10
  • Other methods would be simpler and less __messy__ but if you really want to use this one, then yous should create a __manager.py__ file – BlueMagma Aug 29 '12 at 09:49
  • Does this question give you any ideas? http://stackoverflow.com/questions/4662348/implementing-single-sign-on-sso-using-django – Pramod Aug 29 '12 at 10:42
  • Well, as the sites are all related, using a profile with a ManyToMany to the site objects, in order to be able to re-use a user from one site to another seems an interesting idea. I'll just add some hooks on the login signals when user registers with the same username or tries to login on a site to which he has not registered yet. I just find it somehow disappointing the django provides the user and site frameworks, but won't mix them to segregate users per site :-\ – fylb Aug 29 '12 at 11:39

1 Answers1

2

Instead of this I propose to use a profile :

models.py:

from django.contrib.auth.models import User


class UserProfile(models.Model):
    """ Modèle ajoutant des propriété au modèle User """
    user = models.OneToOneField(User, editable=False)
    site1 = models.BooleanField()
    site2 = models.BooleanField()
    site3 = models.BooleanField()


def create_user_profile(sender, instance, created, **kwargs):
    """ Crée la jonction entre le modèle User, et le modèle UserProfile """
    if created:
        UserProfile.objects.create(user=instance)

post_save.connect(create_user_profile, sender=User)

and on each site you create a decorator :

decorators.py:

try:
    from functools import wraps
except ImportError:
    from django.utils.functional import wraps
from django.http import HttpResponseForbidden
from django.contrib.auth.decorators import login_required
from distrib.views.error import error403


def site1_required(function):
    @wraps(function)
    @login_required
    def decorateur(request, *k, **a):
        if request.user.get_profile().site1 or request.user.is_superuser:
            return function(request, *k, **a)
        else:
            result = error403(request)
            return HttpResponseForbidden(result)
    return decorateur
    return function

then on each view you add the decorator, if the user is not allowed to connect on this site, he will get a http403 error.

BlueMagma
  • 2,392
  • 1
  • 22
  • 46
  • As I commented above, I think I'll rollback to something alike, meaning, I won't mess up with the auth.user model, add a manytomanyfield(Site) in my user profile, and add some hookups on login/register signals to catch/redirect users when they re-use a username. Thanks all :) – fylb Aug 29 '12 at 11:45