A detached user object for Django

Django’s authentication framework (django.contrib.auth) is both pluggable and stackable, which makes integrating custom authentication requirements into Django pretty smooth and easy in most cases.  But I had an edge case: the user ids of the backend system could not be guaranteed to conform to Django’s restriction on user names (even with the recent expansion to accommodate email addresses).  Thus the usual pattern of creating a Django user corresponding to each backend account required a workaround.  At first I tried a hack in which user id from the backend were base64-encoded and munged to conform to Django’s user name limits.  Communication with the backend then required de-munging and decoding the Django user name, etc.  While this seemed to work it was ugly as hell, and in any case, I was using neither the Django admin site nor Django permissions, so I didn’t need a real Django User model instance for that.  On the other hand, I did want to keep the goodness of my pluggable authentication backend and the login view from django.contrib.auth.views, both of which expect User-like objects.

So, I decided to try something kinda crazy: subclassing django.contrib.auth.models.AnonymousUser  and overriding the “anonymity”.  AnonymousUser really takes advantage of Python duck-typing by mimicking the django.contrib.auth.models.User without being a Django model itself, and so isn’t tied to Django’s database.  Here’s what I came up with:

from django.contrib.auth.models import AnonymousUser

class DetachedUser(AnonymousUser):
    """
    Implements a user-like object for user authentication
    when linkage of a backend user identity to a Django user
    is undesirable or unnecessary.
    """

    # mark as not hashable -- see also __eq__() and __ne__(), below
    __hash__ = None 

    # is_active might matter in some contexts, so override
    is_active = True 

    #
    # AnonymousUser sets username to '' and id to None, so we need to at least
    # override those values.
    #
    def __init__(self, username):
        self.username = username
        self.id = username

    def __unicode__(self):
        return self.username

    #
    # is_anonymous() and is_authenticated() are key to distinguishing truly 
    # anonymous/unauthenticated users from known/authenticated ones.
    #
    def is_anonymous(self):
        return False

    def is_authenticated(self):
        return True

    #
    # __eq__ and __ne__ are related to hashing, so be consistent with __hash__, above.
    #
    def __eq__(self, other):
        return NotImplemented

    def __ne__(self, other):
        return NotImplemented

    #
    # Some django.contrib.auth code may call this method,
    # e.g, to update the last login time
    #
    def save(self):
        pass

Now I can code the get_user and authenticate methods of my custom authentication backend to return DetachedUser objects instead of Django users. So far, so good.

Advertisements