diff options
author | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2012-07-12 22:02:16 -0400 |
---|---|---|
committer | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2012-07-12 22:02:16 -0400 |
commit | 1b94c15c0b4c8ffc98a5765aec1309092a1efd1e (patch) | |
tree | a5ecb25449b5eb63321b504c44ef2415e773d673 | |
parent | 5abaedc9eb3408cee1eb42db28bdbd71570626c6 (diff) | |
download | askbot-1b94c15c0b4c8ffc98a5765aec1309092a1efd1e.tar.gz askbot-1b94c15c0b4c8ffc98a5765aec1309092a1efd1e.tar.bz2 askbot-1b94c15c0b4c8ffc98a5765aec1309092a1efd1e.zip |
rehashed the ldap registration flow once again
-rw-r--r-- | askbot/conf/ldap.py | 14 | ||||
-rw-r--r-- | askbot/deps/django_authopenid/backends.py | 197 | ||||
-rw-r--r-- | askbot/deps/django_authopenid/ldap_auth.py | 177 | ||||
-rw-r--r-- | askbot/deps/django_authopenid/views.py | 70 |
4 files changed, 269 insertions, 189 deletions
diff --git a/askbot/conf/ldap.py b/askbot/conf/ldap.py index 30fbcdec..5c37adb1 100644 --- a/askbot/conf/ldap.py +++ b/askbot/conf/ldap.py @@ -19,6 +19,20 @@ settings.register( ) ) +settings.register( + livesettings.BooleanValue( + LDAP_SETTINGS, + 'LDAP_AUTOCREATE_USERS', + description = _('Automatically create user accounts when possible'), + default = False, + help_text = _( + 'Potentially reduces number of steps in the registration process ' + 'but can expose personal information, e.g. when LDAP login name is ' + 'the same as email address or real name.' + ) + ) +) + LDAP_PROTOCOL_VERSION_CHOICES = ( ('3', _('Version 3')), ('2', _('Version 2 (insecure and deprecated)!!!')) diff --git a/askbot/deps/django_authopenid/backends.py b/askbot/deps/django_authopenid/backends.py index f1bdfa4b..7d6e126c 100644 --- a/askbot/deps/django_authopenid/backends.py +++ b/askbot/deps/django_authopenid/backends.py @@ -10,170 +10,12 @@ from django.conf import settings as django_settings from django.utils.translation import ugettext as _ from askbot.deps.django_authopenid.models import UserAssociation from askbot.deps.django_authopenid import util +from askbot.deps.django_authopenid.ldap_auth import ldap_authenticate +from askbot.deps.django_authopenid.ldap_auth import ldap_create_user from askbot.conf import settings as askbot_settings from askbot.models.signals import user_registered -log = logging.getLogger('configuration') - -def split_name(full_name, name_format): - bits = full_name.strip().split() - if len(bits) == 1: - bits.push('') - elif len(bits) == 0: - bits = ['', ''] - - if name_format == 'first,last': - return bits[0], bits[1] - elif name_format == 'last,first': - return bits[1], bits[0] - else: - raise ValueError('Unexpected value of name_format') - - -def ldap_authenticate(username, password): - """ - Authenticate using ldap - - python-ldap must be installed - http://pypi.python.org/pypi/python-ldap/2.4.6 - """ - import ldap - user_information = None - try: - ldap_session = ldap.initialize(askbot_settings.LDAP_URL) - - #set protocol version - if askbot_settings.LDAP_PROTOCOL_VERSION == '2': - ldap_session.protocol_version = ldap.VERSION2 - elif askbot_settings.LDAP_PROTOCOL_VERSION == '3': - ldap_session.protocol_version = ldap.VERSION3 - else: - raise NotImplementedError('unsupported version of ldap protocol') - - ldap.set_option(ldap.OPT_REFERRALS, 0) - - #set extra ldap options, if given - if hasattr(django_settings, 'LDAP_EXTRA_OPTIONS'): - options = django_settings.LDAP_EXTRA_OPTIONS - for key, value in options: - if key.startswith('OPT_'): - ldap_key = getattr(ldap, key) - ldap.set_option(ldap_key, value) - else: - raise ValueError('Invalid LDAP option %s' % key) - - #add optional "master" LDAP authentication, if required - master_username = getattr(django_settings, 'LDAP_LOGIN_DN', None) - master_password = getattr(django_settings, 'LDAP_PASSWORD', None) - - login_name_field = askbot_settings.LDAP_LOGIN_NAME_FIELD - base_dn = askbot_settings.LDAP_BASE_DN - login_template = login_name_field + '=%s,' + base_dn - encoding = askbot_settings.LDAP_ENCODING - - if master_username and master_password: - ldap_session.simple_bind_s( - master_username.encode(encoding), - master_password.encode(encoding) - ) - - user_filter = askbot_settings.LDAP_USER_FILTER_TEMPLATE % ( - askbot_settings.LDAP_LOGIN_NAME_FIELD, - username - ) - - email_field = askbot_settings.LDAP_EMAIL_FIELD - - get_attrs = [ - email_field.encode(encoding), - login_name_field.encode(encoding) - #str(askbot_settings.LDAP_USERID_FIELD) - #todo: here we have a chance to get more data from LDAP - #maybe a point for some plugin - ] - - common_name_field = askbot_settings.LDAP_COMMON_NAME_FIELD.strip() - given_name_field = askbot_settings.LDAP_GIVEN_NAME_FIELD.strip() - surname_field = askbot_settings.LDAP_SURNAME_FIELD.strip() - - if given_name_field and surname_field: - get_attrs.append(given_name_field.encode(encoding)) - get_attrs.append(surname_field.encode(encoding)) - elif common_name_field: - get_attrs.append(common_name_field.encode(encoding)) - - # search ldap directory for user - user_search_result = ldap_session.search_s( - askbot_settings.LDAP_BASE_DN.encode(encoding), - ldap.SCOPE_SUBTREE, - user_filter.encode(encoding), - get_attrs - ) - if user_search_result: # User found in LDAP Directory - user_dn = user_search_result[0][0] - user_information = user_search_result[0][1] - ldap_session.simple_bind_s(user_dn, password.encode(encoding)) #raises INVALID_CREDENTIALS - ldap_session.unbind_s() - - exact_username = user_information[login_name_field][0] - email = user_information.get(email_field, [''])[0] - - if given_name_field and surname_field: - last_name = user_information.get(surname_field, [''])[0] - first_name = user_information.get(given_name_field, [''])[0] - elif surname_field: - common_name_format = askbot_settings.LDAP_COMMON_NAME_FIELD_FORMAT - common_name = user_information.get(common_name_field, [''])[0] - first_name, last_name = split_name(common_name, common_name_format) - - #here we have an opportunity to copy password in the auth_user table - #but we don't do it for security reasons - try: - user = User.objects.get(username__exact=exact_username) - # always update user profile to synchronize with ldap server - user.set_unusable_password() - #user.first_name = first_name - #user.last_name = last_name - user.email = email - user.save() - except User.DoesNotExist: - # create new user in local db - user = User() - user.username = exact_username - user.set_unusable_password() - #user.first_name = first_name - #user.last_name = last_name - user.email = email - user.is_staff = False - user.is_superuser = False - user.is_active = True - user.save() - user_registered.send(None, user = user) - - log.info('Created New User : [{0}]'.format(exact_username)) - return user - else: - # Maybe a user created internally (django admin user) - try: - user = User.objects.get(username__exact=username) - if user.check_password(password): - return user - else: - return None - except User.DoesNotExist: - return None - - except ldap.INVALID_CREDENTIALS, e: - return None # Will fail login on return of None - except ldap.LDAPError, e: - log.error("LDAPError Exception") - log.exception(e) - return None - except Exception, e: - log.error("Unexpected Exception Occurred") - log.exception(e) - return None - +LOG = logging.getLogger(__name__) class AuthBackend(object): """Authenticator's authentication backend class @@ -183,6 +25,8 @@ class AuthBackend(object): the reason there is only one class - for simplicity of adding this application to a django project - users only need to extend the AUTHENTICATION_BACKENDS with a single line + + todo: it is not good to have one giant do all 'authenticate' function """ def authenticate( @@ -222,7 +66,7 @@ class AuthBackend(object): except User.DoesNotExist: return None except User.MultipleObjectsReturned: - logging.critical( + LOG.critical( ('have more than one user with email %s ' + 'he/she will not be able to authenticate with ' + 'the email address in the place of user name') % email_address @@ -323,7 +167,34 @@ class AuthBackend(object): return None elif method == 'ldap': - user = ldap_authenticate(username, password) + user_info = ldap_authenticate(username, password) + if user_info is None: + # Maybe a user created internally (django admin user) + try: + user = User.objects.get(username__exact=username) + if user.check_password(password): + return user + else: + return None + except User.DoesNotExist: + return None + else: + #load user by association or maybe auto-create one + ldap_username = user_info['ldap_username'] + try: + #todo: provider_name is hardcoded - possible conflict + assoc = UserAssociation.objects.get( + openid_url = ldap_username + '@ldap', + provider_name = 'ldap' + ) + user = assoc.user + except UserAssociation.DoesNotExist: + #email address is required + if 'email' in user_info and askbot_settings.LDAP_AUTOCREATE_USERS: + assoc = ldap_create_user(user_info) + user = assoc.user + else: + return None elif method == 'wordpress_site': try: diff --git a/askbot/deps/django_authopenid/ldap_auth.py b/askbot/deps/django_authopenid/ldap_auth.py new file mode 100644 index 00000000..e48ec943 --- /dev/null +++ b/askbot/deps/django_authopenid/ldap_auth.py @@ -0,0 +1,177 @@ +import logging +from django.conf import settings as django_settings +from django.contrib.auth.models import User +from django.forms import EmailField +from askbot.conf import settings as askbot_settings +from askbot.models.signals import user_registered + +LOG = logging.getLogger(__name__) + +def split_name(full_name, name_format): + bits = full_name.strip().split() + if len(bits) == 1: + bits.push('') + elif len(bits) == 0: + bits = ['', ''] + + if name_format == 'first,last': + return bits[0], bits[1] + elif name_format == 'last,first': + return bits[1], bits[0] + else: + raise ValueError('Unexpected value of name_format') + + +def ldap_authenticate(username, password): + """ + Authenticate using ldap + + returns a dict with keys: + + * first_name + * last_name + * ldap_username + * email (optional only if there is valid email) + + python-ldap must be installed + http://pypi.python.org/pypi/python-ldap/2.4.6 + """ + import ldap + user_information = None + try: + ldap_session = ldap.initialize(askbot_settings.LDAP_URL) + + #set protocol version + if askbot_settings.LDAP_PROTOCOL_VERSION == '2': + ldap_session.protocol_version = ldap.VERSION2 + elif askbot_settings.LDAP_PROTOCOL_VERSION == '3': + ldap_session.protocol_version = ldap.VERSION3 + else: + raise NotImplementedError('unsupported version of ldap protocol') + + ldap.set_option(ldap.OPT_REFERRALS, 0) + + #set extra ldap options, if given + if hasattr(django_settings, 'LDAP_EXTRA_OPTIONS'): + options = django_settings.LDAP_EXTRA_OPTIONS + for key, value in options: + if key.startswith('OPT_'): + ldap_key = getattr(ldap, key) + ldap.set_option(ldap_key, value) + else: + raise ValueError('Invalid LDAP option %s' % key) + + #add optional "master" LDAP authentication, if required + master_username = getattr(django_settings, 'LDAP_LOGIN_DN', None) + master_password = getattr(django_settings, 'LDAP_PASSWORD', None) + + login_name_field = askbot_settings.LDAP_LOGIN_NAME_FIELD + base_dn = askbot_settings.LDAP_BASE_DN + login_template = login_name_field + '=%s,' + base_dn + encoding = askbot_settings.LDAP_ENCODING + + if master_username and master_password: + ldap_session.simple_bind_s( + master_username.encode(encoding), + master_password.encode(encoding) + ) + + user_filter = askbot_settings.LDAP_USER_FILTER_TEMPLATE % ( + askbot_settings.LDAP_LOGIN_NAME_FIELD, + username + ) + + email_field = askbot_settings.LDAP_EMAIL_FIELD + + get_attrs = [ + email_field.encode(encoding), + login_name_field.encode(encoding) + #str(askbot_settings.LDAP_USERID_FIELD) + #todo: here we have a chance to get more data from LDAP + #maybe a point for some plugin + ] + + common_name_field = askbot_settings.LDAP_COMMON_NAME_FIELD.strip() + given_name_field = askbot_settings.LDAP_GIVEN_NAME_FIELD.strip() + surname_field = askbot_settings.LDAP_SURNAME_FIELD.strip() + + if given_name_field and surname_field: + get_attrs.append(given_name_field.encode(encoding)) + get_attrs.append(surname_field.encode(encoding)) + elif common_name_field: + get_attrs.append(common_name_field.encode(encoding)) + + # search ldap directory for user + user_search_result = ldap_session.search_s( + askbot_settings.LDAP_BASE_DN.encode(encoding), + ldap.SCOPE_SUBTREE, + user_filter.encode(encoding), + get_attrs + ) + if user_search_result: # User found in LDAP Directory + user_dn = user_search_result[0][0] + user_information = user_search_result[0][1] + ldap_session.simple_bind_s(user_dn, password.encode(encoding)) #raises INVALID_CREDENTIALS + ldap_session.unbind_s() + + if given_name_field and surname_field: + last_name = user_information.get(surname_field, [''])[0] + first_name = user_information.get(given_name_field, [''])[0] + elif surname_field: + common_name_format = askbot_settings.LDAP_COMMON_NAME_FIELD_FORMAT + common_name = user_information.get(common_name_field, [''])[0] + first_name, last_name = split_name(common_name, common_name_format) + + user_info = { + 'first_name': first_name, + 'last_name': last_name, + 'ldap_username': user_information[login_name_field][0] + } + + try: + email = user_information.get(email_field, [''])[0] + user_info['email'] = EmailField().clean(email) + except ValidationError: + pass + + return user_info + else: + return None + + except ldap.INVALID_CREDENTIALS, e: + return None # Will fail login on return of None + except ldap.LDAPError, e: + LOG.error("LDAPError Exception") + LOG.exception(e) + return None + except Exception, e: + LOG.error("Unexpected Exception Occurred") + LOG.exception(e) + return None + + +def ldap_create_user(user_info): + """takes the result returned by the :func:`ldap_authenticate` + + and returns a :class:`UserAssociation` object + """ + # create new user in local db + user = User() + user.username = user_info['ldap_username'] + user.set_unusable_password() + user.first_name = user_info['first_name'] + user.last_name = user_info['last_name'] + user.email = user_info['email'] + user.is_staff = False + user.is_superuser = False + user.is_active = True + user.save() + user_registered.send(None, user = user) + LOG.info('Created New User : [{0}]'.format(exact_username)) + + assoc = UserAssociation() + assoc.user = user + assoc.openid_url = user_info['ldap_username'] + '@ldap' + assoc.provider_name = 'ldap' + assoc.save() + return assoc diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py index 9b270230..47d8626f 100644 --- a/askbot/deps/django_authopenid/views.py +++ b/askbot/deps/django_authopenid/views.py @@ -48,6 +48,8 @@ from django.utils.safestring import mark_safe from django.core.mail import send_mail from recaptcha_works.decorators import fix_recaptcha_remote_ip from askbot.skins.loaders import render_into_skin, get_template +from askbot.deps.django_authopenid.ldap_auth import ldap_create_user +from askbot.deps.django_authopenid.ldap_auth import ldap_authenticate from urlparse import urlparse from openid.consumer.consumer import Consumer, \ @@ -312,25 +314,39 @@ def signin(request): assert(password_action == 'login') username = login_form.cleaned_data['username'] password = login_form.cleaned_data['password'] - # will be None if authentication fails - #todo: since django 1.2 there is .exists() - user_is_old = (User.objects.filter(username = username).count() > 0) user = authenticate( username=username, password=password, method = 'ldap' ) - if user is not None: - login(request, user) - if user_is_old: - return HttpResponseRedirect(next_url) - else: - return HttpResponseRedirect(reverse('verify_user_information')) + if user: + login(request, user) + return HttpResponseRedirect(next_url) else: - request.user.message_set.create(_('Incorrect user name or password')) - return HttpResponseRedirect(request.path) + #try to login again via LDAP + user_info = ldap_authenticate(username, password) + if user_info: + if askbot_settings.LDAP_AUTOCREATE_USERS: + #create new user or + user = ldap_create_user(user_info).user + login(request, user) + return HttpResponseRedirect(next_url) + else: + #continue with proper registration + ldap_username = user_info['ldap_username'] + return finalize_generic_signin( + request, + login_provider_name = 'ldap', + user_identifier = ldap_username + '@ldap', + redirect_url = next_url + ) + else: + request.user.message_set.create( + _('Incorrect user name or password') + ) + return HttpResponseRedirect(request.path) else: if password_action == 'login': user = authenticate( @@ -747,22 +763,21 @@ def finalize_generic_signin( {'provider': login_provider_name} request.user.message_set.create(message = msg) return HttpResponseRedirect(redirect_url) + elif user: + #login branch + login(request, user) + logging.debug('login success') + return HttpResponseRedirect(redirect_url) else: - if user is None: - #need to register - request.method = 'GET'#this is not a good thing to do - #but necessary at the moment to reuse the register() - #method - return register( - request, - login_provider_name=login_provider_name, - user_identifier=user_identifier - ) - else: - #login branch - login(request, user) - logging.debug('login success') - return HttpResponseRedirect(redirect_url) + #need to register + request.method = 'GET'#this is not a good thing to do + #but necessary at the moment to reuse the register() + #method + return register( + request, + login_provider_name=login_provider_name, + user_identifier=user_identifier + ) @login_required @csrf.csrf_protect @@ -820,6 +835,9 @@ def register(request, login_provider_name=None, user_identifier=None): in which case request.method must ge 'GET' and login_provider_name and user_identifier arguments must not be None + user_identifier will be stored in the UserAssociation as openid_url + login_provider_name - as provider_name + this function may need to be refactored to simplify the usage pattern template : authopenid/complete.html |