diff --git a/helios_auth/auth_systems/__init__.py b/helios_auth/auth_systems/__init__.py index 5a0e9233ba4df89067688283e90649aef4f1ae70..aaaddb56b6b5a29d4a414b4c34b2ba4a11278085 100644 --- a/helios_auth/auth_systems/__init__.py +++ b/helios_auth/auth_systems/__init__.py @@ -2,6 +2,9 @@ AUTH_SYSTEMS = {} import twitter, password, cas, facebook, google, yahoo, linkedin, clever +import ldapauth + + AUTH_SYSTEMS['twitter'] = twitter AUTH_SYSTEMS['linkedin'] = linkedin AUTH_SYSTEMS['password'] = password @@ -10,6 +13,7 @@ AUTH_SYSTEMS['facebook'] = facebook AUTH_SYSTEMS['google'] = google AUTH_SYSTEMS['yahoo'] = yahoo AUTH_SYSTEMS['clever'] = clever +AUTH_SYSTEMS['ldap'] = ldapauth # not ready #import live diff --git a/helios_auth/auth_systems/ldapauth.py b/helios_auth/auth_systems/ldapauth.py new file mode 100644 index 0000000000000000000000000000000000000000..4e201f94ce2fef763c8891ea3ca57240fac76fb9 --- /dev/null +++ b/helios_auth/auth_systems/ldapauth.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +""" +LDAP Authentication +Author : shirlei@gmail.com +Version: 1.0 +Requires libldap2-dev +django-auth-ldap 1.2.7 +LDAP authentication relies on django-auth-ldap (http://pythonhosted.org/django-auth-ldap/), +which considers that "Authenticating against an external source is swell, but Django’s +auth module is tightly bound to a user model. When a user logs in, we have to create a model +object to represent them in the database." +Helios, originally, does not rely on default django user model. Discussion about that can be +found in: +https://groups.google.com/forum/#!topic/helios-voting/nRHFAbAHTNA +That considered, using a django plugin for ldap authentication, in order to not reinvent the +wheel seems ok, since it does not alter anything on original helios user model, it is just +for authentication purposes. +However, two installed_apps that are added when you first create a django project, which were +commented out in helios settings, need to be made available now: +django.contrib.auth +django.contrib.contenttypes' +This will enable the native django authentication support on what django-auth-ldap is build upon. +Further reference on +https://docs.djangoproject.com/en/1.8/topics/auth/ +""" + +from django import forms +from django.conf import settings +from django.core.mail import send_mail +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect + + +from helios_auth.auth_systems.ldapbackend import backend + + +# some parameters to indicate that status updating is possible +STATUS_UPDATES = False + + +LOGIN_MESSAGE = "Log in with my LDAP Account" + +class LoginForm(forms.Form): + username = forms.CharField(max_length=250) + password = forms.CharField(widget=forms.PasswordInput(), max_length=100) + + +def ldap_login_view(request): + from helios_auth.view_utils import render_template + from helios_auth.views import after + + error = None + + if request.method == "GET": + form = LoginForm() + else: + form = LoginForm(request.POST) + + request.session['auth_system_name'] = 'ldap' + + if request.POST.has_key('return_url'): + request.session['auth_return_url'] = request.POST.get('return_url') + + if form.is_valid(): + username = form.cleaned_data['username'].strip() + password = form.cleaned_data['password'].strip() + + auth = backend.CustomLDAPBackend() + user = auth.authenticate(username, password) + + if user: + request.session['ldap_user'] = { + 'user_id': user.email, + 'name': user.first_name + ' ' + user.last_name, + } + return HttpResponseRedirect(reverse(after)) + else: + error = 'Bad Username or Password' + + return render_template(request, 'password/login', { + 'form': form, + 'error': error, + 'enabled_auth_systems': settings.AUTH_ENABLED_AUTH_SYSTEMS, + }) + + +def get_user_info_after_auth(request): + return { + 'type': 'ldap', + 'user_id' : request.session['ldap_user']['user_id'], + 'name': request.session['ldap_user']['name'], + 'info': {'email': request.session['ldap_user']['user_id']}, + 'token': None + } + + +def get_auth_url(request, redirect_url = None): + return reverse(ldap_login_view) + + +def send_message(user_id, name, user_info, subject, body): + send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (name, user_id)], fail_silently=False) + + +def check_constraint(constraint, user_info): + """ + for eligibility + """ + pass + +def can_create_election(user_id, user_info): + return True + diff --git a/helios_auth/auth_systems/ldapbackend/__init__.py b/helios_auth/auth_systems/ldapbackend/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/helios_auth/auth_systems/ldapbackend/backend.py b/helios_auth/auth_systems/ldapbackend/backend.py new file mode 100644 index 0000000000000000000000000000000000000000..ef1930dcb81b3be2c052d0b6682f1a08ab3f7998 --- /dev/null +++ b/helios_auth/auth_systems/ldapbackend/backend.py @@ -0,0 +1,28 @@ +from django.conf import settings + +from django_auth_ldap.backend import LDAPBackend +from django_auth_ldap.config import LDAPSearch +from django_auth_ldap.backend import populate_user + + +class CustomLDAPBackend(LDAPBackend): + def authenticate(self, username, password): + """ + Some ldap servers allow anonymous search but naturally return just a set + of user attributes. So, here we re-perform search after user is authenticated, + in order to populate other user attributes. + For now, just in cases where AUTH_LDAP_BIND_PASSWORD is empty + """ + user = super(CustomLDAPBackend, self).authenticate(username, password) + + if user and settings.AUTH_LDAP_BIND_PASSWORD == '' : + search = self.settings.USER_SEARCH + if search is None: + raise ImproperlyConfigured('AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance.') + results = search.execute(user.ldap_user.connection, {'user': user.username}) + if results is not None and len(results) == 1: + (user.ldap_user._user_dn, user.ldap_user.user_attrs) = results[0] + user.ldap_user._load_user_attrs() + user.ldap_user._populate_user_from_attributes() + user.save() + return user diff --git a/helios_auth/media/login-icons/ldap.png b/helios_auth/media/login-icons/ldap.png new file mode 100644 index 0000000000000000000000000000000000000000..86f7807cbcf9f8d5add2c9450a1cf74b30ff1cb6 Binary files /dev/null and b/helios_auth/media/login-icons/ldap.png differ diff --git a/helios_auth/tests.py b/helios_auth/tests.py index f07f309fb5176007ca67a25ce14c712efc33af35..9b097cc97e0a346e3d13da87f1eb9156f1ee3094 100644 --- a/helios_auth/tests.py +++ b/helios_auth/tests.py @@ -13,6 +13,7 @@ from django.test import TestCase from django.core import mail from auth_systems import AUTH_SYSTEMS +from helios_auth import ENABLED_AUTH_SYSTEMS class UserModelTests(unittest.TestCase): @@ -128,3 +129,43 @@ class UserBlackboxTests(TestCase): self.assertEquals(len(mail.outbox), 1) self.assertEquals(mail.outbox[0].subject, "testing subject") self.assertEquals(mail.outbox[0].to[0], "\"Foobar User\" <foobar-test@adida.net>") + + +import auth_systems.ldapauth as ldap_views + + +class LDAPAuthTests(TestCase): + """ + These tests relies on OnLine LDAP Test Server, provided by forum Systems: + http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ + """ + + def setUp(self): + """ set up necessary django-auth-ldap settings """ + self.password = 'password' + self.username = 'euclid' + + def test_backend_login(self): + """ test if authenticates using the backend """ + if 'ldap' in ENABLED_AUTH_SYSTEMS: + from helios_auth.auth_systems.ldapbackend import backend + auth = backend.CustomLDAPBackend() + user = auth.authenticate(self.username, self.password) + self.assertEqual(user.username, 'euclid') + + def test_ldap_view_login(self): + """ test if authenticates using the auth system login view """ + if 'ldap' in ENABLED_AUTH_SYSTEMS: + resp = self.client.post(reverse(ldap_views.ldap_login_view), { + 'username' : self.username, + 'password': self.password + }, follow=True) + self.assertEqual(resp.status_code, 200) + + def test_logout(self): + """ test if logs out using the auth system logout view """ + if 'ldap' in ENABLED_AUTH_SYSTEMS: + response = self.client.post(reverse(views.logout), follow=True) + self.assertContains(response, "not logged in") + self.assertNotContains(response, "euclid") + diff --git a/helios_auth/urls.py b/helios_auth/urls.py index e4dca398503d1e2f802fd4811330b66b9020a1a9..810d4f8f9e054bd413687e1cbde6af05c34f9bf7 100644 --- a/helios_auth/urls.py +++ b/helios_auth/urls.py @@ -9,6 +9,8 @@ from django.conf.urls import * from views import * from auth_systems.password import password_login_view, password_forgotten_view from auth_systems.twitter import follow_view +from auth_systems.ldapauth import ldap_login_view + urlpatterns = patterns('', # basic static stuff @@ -28,4 +30,7 @@ urlpatterns = patterns('', # twitter (r'^twitter/follow', follow_view), + + #ldap + (r'^ldap/login', ldap_login_view), ) diff --git a/requirements.txt b/requirements.txt index 2cc4eb872cc1522a3d333dceee03caf92aa9d8d9..eb774f3c76615017da3351d362d8ed7b56a6687f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,4 @@ boto==2.27.0 django-ses==0.6.0 validate_email==1.2 oauth2client==1.2 +django-auth-ldap==1.2.7 diff --git a/settings.py b/settings.py index f1e2ab28b09a12f6e826af7d676aa31f06f6575c..2f440d511388492d26cbb67c279012fb0447b3d0 100644 --- a/settings.py +++ b/settings.py @@ -1,6 +1,7 @@ - +import ldap import os, json +from django_auth_ldap.config import LDAPSearch, GroupOfNamesType # a massive hack to see if we're testing, in which case we use different settings import sys TESTING = 'test' in sys.argv @@ -133,8 +134,8 @@ TEMPLATE_DIRS = ( ) INSTALLED_APPS = ( -# 'django.contrib.auth', -# 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.contenttypes', 'djangosecure', 'django.contrib.sessions', #'django.contrib.sites', @@ -206,7 +207,7 @@ HELIOS_VOTERS_EMAIL = True HELIOS_PRIVATE_DEFAULT = False # authentication systems enabled -#AUTH_ENABLED_AUTH_SYSTEMS = ['password','facebook','twitter', 'google', 'yahoo'] +#AUTH_ENABLED_AUTH_SYSTEMS = ['password','facebook','twitter', 'google', 'yahoo','ldap'] AUTH_ENABLED_AUTH_SYSTEMS = get_from_env('AUTH_ENABLED_AUTH_SYSTEMS', 'google').split(",") AUTH_DEFAULT_AUTH_SYSTEM = get_from_env('AUTH_DEFAULT_AUTH_SYSTEM', None) @@ -242,6 +243,27 @@ CAS_ELIGIBILITY_REALM = get_from_env('CAS_ELIGIBILITY_REALM', "") CLEVER_CLIENT_ID = get_from_env('CLEVER_CLIENT_ID', "") CLEVER_CLIENT_SECRET = get_from_env('CLEVER_CLIENT_SECRET', "") +# ldap +# see configuration example at https://pythonhosted.org/django-auth-ldap/example.html +AUTH_LDAP_SERVER_URI = "ldap://ldap.forumsys.com" # replace by your Ldap URI +AUTH_LDAP_BIND_DN = "cn=read-only-admin,dc=example,dc=com" +AUTH_LDAP_BIND_PASSWORD = "password" +AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com", + ldap.SCOPE_SUBTREE, "(uid=%(user)s)" +) +# Populate the Django user from the LDAP directory. +AUTH_LDAP_USER_ATTR_MAP = { + "first_name": "givenName", + "last_name": "sn", + "email": "mail", +} +AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn") +AUTH_LDAP_FIND_GROUP_PERMS = True +AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True +AUTH_LDAP_CACHE_GROUPS = True +AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600 +AUTH_LDAP_ALWAYS_UPDATE_USER = False + # email server EMAIL_HOST = get_from_env('EMAIL_HOST', 'localhost') EMAIL_PORT = int(get_from_env('EMAIL_PORT', "2525"))