How to get unique users across multiple Django sites powered by the "sites" framework?
Asked Answered
I

3

26

I am building a Django site framework which will power several independent sites, all using the same apps but with their own templates. I plan to accomplish this by using multiple settings-files and setting a unique SITE_ID for them, like suggested in the Django docs for the django.contrib.sites framework

However, I don't want a user from site A to be able to login on site B. After inspecting the user table created by syncdb, I can see no column which might restrict a user to a specific site. I have also tried to create a user, 'bob', on one site and then using the shell command to list all users on the other side and sure enough, bob shows up there.

How can I ensure all users are restricted to their respective sites?

Inerasable answered 10/9, 2009 at 8:46 Comment(1)
Great question - don't know that it's possible, but maybe someone else knows better.Grannie
S
28

The most compatible way to do this would be to create a user Profile model that includes a foreign key to the Site model, then write a custom auth backend that checks the current site against the value of that FK. Some sample code:

Define your profile model, let's say in app/models.py:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.auth.models import User

class UserProfile(models.Model):
    user = models.OneToOneField(User)
    site = models.ForeignKey(Site)

Write your custom auth backend, inheriting from the default one, let's say in app/auth_backend.py:

from django.contrib.auth.backends import ModelBackend
from django.contrib.sites.models import Site

class SiteBackend(ModelBackend):
    def authenticate(self, **credentials):
        user_or_none = super(SiteBackend, self).authenticate(**credentials)
        if user_or_none and user_or_none.userprofile.site != Site.objects.get_current():
            user_or_none = None
        return user_or_none

    def get_user(self, user_id):
        try:
            return User.objects.get(
                pk=user_id, userprofile__site=Site.objects.get_current())
        except User.DoesNotExist:
            return None

This auth backend assumes all users have a profile; you'd need to make sure that your user creation/registration process always creates one.

The overridden authenticate method ensures that a user can only login on the correct site. The get_user method is called on every request to fetch the user from the database based on the stored authentication information in the user's session; our override ensures that a user can't login on site A and then use that same session cookie to gain unauthorized access to site B. (Thanks to Jan Wrobel for pointing out the need to handle the latter case.)

Sherburne answered 10/9, 2009 at 15:12 Comment(8)
Thanks Carl, I'll use your implementation and hopefully learn something about custon backends in the process.Inerasable
Noe that this backend does only part of the job. It disallows user of one site to login to the other site, which is not enough. A user can log into the site A and manually copy authentication cookie to send it with request to the site B. Such cookie will be recognized as valid and the user will be logged in (authentication backend is invoked only when there is no valid session cookie). To complete the solution, you need a middleware that will be invoked for each request to your sites and that will check that user is logged into the site the request is related to, not some other site.Griff
@JanWrobel I believe that rather than a separate middleware, you can just implement a similar check in the get_user method of your authentication backend. This method is called on every request, not just at login time. I'll update the answer to include this; thanks for pointing out the omission.Sherburne
@CarlMeyer overriding get_user works and is indeed much simpler than a separate middleware. Thanks!Griff
but what to do with unique fields? how to add two users with username 'Arti' in different sites ?Venditti
@Venditti you can't address that using a separate profile model approach. Instead you'd want to use the "custom user model" feature in Django 1.5+, and write a user model that includes the FK to Site, and which makes username a unique_together with site rather than fully unique.Sherburne
@CarlMeyer can an user use the same email address to registrate in site A and B with this solution? The User.email unique restriction will prohibit it?Supernal
In the default user model, email is not unique but username is. One way you could address that requirement is by making the link from profile to user an FK instead of a OneToOneField, and let a single user have profiles on multiple sites.Sherburne
R
3

You can plug your own authorization and authentication backends that take the site id into consideration.

See other authentication sources on the django documentation and the authentication backends references

Besides that, if your django source is too old, you can always modify the authenticate() or login() code yourself. After all... Isn't that one of the wonders of open source. Be aware that by doing so you may affect your compatibility with other modules.

Hope this helps.

Reduplicative answered 10/9, 2009 at 9:9 Comment(3)
Writing a custom backend is certainly something I have considered, but turned down for the moment as I don't think I have the skills necessary. The same goes for modifying the actual Django code.Inerasable
Writing a custom auth backend is not hard, it's probably about six lines of code. One of the easier things to do in Django. To maintain maximum compatibility, put a ForeignKey to Site in the user profile model and key off that in your custom auth backend.Sherburne
I would upvote this answer if it didn't recommend modifying Django source directly. There's absolutely no reason to do that when custom auth backends are available, it shouldn't even be mentioned in the answer.Sherburne
J
-1

You have to know, that many people complain for Django default authorization system and privileges - it has simply rules for objects, for instances of the objects - what it means, that without writing any code it woudn't be possible.

However, there are some authorization hooks which can helps you to achieve this goal, for example:

Take a look there: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/models.py and for class Permission.

You can add your own permission and define rules for them (there is a ForeignKey for User and for ContentType).

However2, without monkeypatching/change some methods it could be difficult.

Justus answered 10/9, 2009 at 13:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.