2

My Django application has two types of users, each with their own authentication mechanism: regular users log in with a username and password, and employees log in via the company's SSO server using django-cas-client.

For this purpose I need two user tables: one for regular users and one for employees. A single table won't cut it, because the usernames might overlap. That is, usernames are unique for regular users, and are unique for employees, but a regular user and an employee might have the same username.

Similar questions like this one, this one and this one don't apply to my situation, because in my case the different user types can have the same username, while still being different users. Unfortunately, I have no influence over the usernames that are returned by the CAS server.

This is what I've got so far:

Instead of creating two user models, I created two profile models, linked via a OneToOneField to the user model:

from django.conf import settings

class RegularUser(models.Model)
    user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='regular_user')
    nickname = models.CharField(max_length=255, blank=True)

class Employee(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='employee')
    first_name = models.CharField(max_length=255, blank=True)
    full_name = models.CharField(max_length=255, blank=True)
    NR = models.CharField(max_length=255, blank=True)
    email = models.CharField(max_length=255, blank=True)

I then used a django-cas-client CAS Response Callback to automatically populate newly created CAS users with the employee_number retrieved via LDAP:

from django.contrib.auth import get_user_model
from .models import Employee

def callback(tree):
    username = tree[0][0].text
    user, created = get_user_model().objects.get_or_create(username=username)
    employee, created = Employee.objects.get_or_create(user=user)

    try:
        (
            employee.first_name,
            employee.full_name,
            employee.NR,
            employee.email
        ) = search_ldap(username)
        employee.save()

    except LDAPError:
        pass

For those interested, this is the implementation of the search_ldap() function:

def search_ldap(username):
    result = ()
    baseDN = "o=Our Organization Name,c=NL"
    searchFilter = '(uid={})'.format(username)
    attributes = ['givenName', 'cn', 'employeeNumber', 'mail']

    try:
        server = Server('ldap.example.com', use_ssl=True)
        conn = Connection(server, auto_bind=True)
        conn.search(baseDN, searchFilter, attributes=attributes)
        for a in attributes:
            result += (conn.response[0]['attributes'][a][0], )
    except Exception:
        raise LDAPError('Error in LDAP query')

    return result

The end result is that only users that have logged in via the CAS server possess the employee attribute. I haven't got around to automatically creating regular users, but of course only they will possess the regular_user attribute. To log users in, I have defined two separate login pages in my urls.py:

import cas.views
import django.contrib.auth.views
from django.conf.urls import include, url
from .views import login, logout

urlpatterns = [
    url(r'^login/regular/$', django.contrib.auth.views.login, name='login_regular'),
    url(r'^logout/regular/$', django.contrib.auth.views.logout, name='logout_regular'),
    url(r'^login/sso/$',cas.views.login, name='login_sso'),
    url(r'^logout/sso/$',cas.views.logout, name='logout_sso'),
    url(r'^login/$', login, name='login'),
    url(r'^logout/$', logout, name='logout'),
]

As you can see, there are also general login and logout urls. I have implemented these views as follows:

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required

def login(request):
    param = request.META['QUERY_STRING']
    return render(request, 'login.html', {
        'param': param,
    })

@login_required
def logout(request):
    if hasattr(request.user, 'employee'):
        return redirect('logout_sso')
    else:
        return redirect('logout_regular')

The general login template simply displays the links the to login_sso URL and the login_regular URL. The logout view always returns a redirect to the correct logout URL.

This code works perfectly and as intended, with one major problem: One day, the CAS server might return an employee username that is, by pure coincedence, the same as an already existing regular user. The CAS Response Callback will happily retrieve the existing user and initiate the employee profile. The end result is that this user now has both a regular_user attribute and an employee attribute. Even worse, the account of the original regular user has now been hijacked by the employee who happens to have the same username. I would like to have regular users and employees with the same username coexist peacefully. How can I accomplish this?

Community
  • 1
  • 1
Jaap Joris Vens
  • 3,382
  • 2
  • 26
  • 42
  • I think you'll find it tricky to have multiple user models. I might try to create a single custom user model with two fields `regular_username` and `employee_username`. – Alasdair May 11 '16 at 10:13
  • Thanks @Alasdair for this great suggestion! I have implemented a slight variation of it. See the updated question. – Jaap Joris Vens May 11 '16 at 13:01

0 Answers0