Merge pull request #75 from dctrud/ldapsupport2

LDAP login with username
This commit is contained in:
Dannon Baker
2015-04-06 15:42:14 -04:00
12 changed files with 124 additions and 91 deletions
+1 -1
View File
@@ -856,7 +856,7 @@ use_interactive = True
#openid_consumer_cache_path = database/openid_consumer_cache
# XML config file that allows the use of different authentication providers
# (e.g. Active Directory) instead or in addition to local authentication
# (e.g. LDAP) instead or in addition to local authentication
# (.sample is used if default does not exist).
#auth_config_file = config/auth_conf.xml
+29 -23
View File
@@ -20,19 +20,24 @@ log = logging.getLogger(__name__)
# <auth>
# <authenticator>
# <type>activedirectory</type>
# <filter>'[username]'.endswith('@students.latrobe.edu.au')</filter>
# <type>ldap_ad</type>
# <filter>'[login]'.endswith('@students.latrobe.edu.au')</filter>
# <options>
# <auto-register>True</auto-register>
# <server>ldap://STUDENTS.ltu.edu.au</server>
# [<search-filter>(&amp;(objectClass=user)(mail={username}))</search-filter>
# [<search-filter>(&amp;(objectClass=user)(mail={login}))</search-filter>
# <search-base>dc=STUDENTS,dc=ltu,dc=edu,dc=au</search-base>
# <!-- If search-user not specified will bind anonymously to LDAP for search -->
# <search-user>jsmith</search-user>
# <search-password>mysecret</search-password>
# <search-fields>sAMAccountName</search-fields>]
# <search-fields>sAMAccountName,mail</search-fields>]
# <bind-user>{sAMAccountName}@STUDENTS.ltu.edu.au</bind-user>
# <bind-password>{password}</bind-password>
# <auto-register-username>{sAMAccountName}</auto-register-username>
# <auto-register-email>{mail}</auto-register-email>
# <!-- To allow login with username instead of email
# <login-use-username>True</login-use-username>
# -->
# </options>
# </authenticator>
# ...
@@ -44,7 +49,7 @@ class AuthManager(object):
def __init__(self, app):
self.__app = app
import galaxy.auth.providers
self.__plugins_dict = plugin_config.plugins_dict( galaxy.auth.providers, 'plugin_type' )
self.__plugins_dict = plugin_config.plugins_dict(galaxy.auth.providers, 'plugin_type' )
auth_config_file = app.config.auth_config_file
self.__init_authenticators(auth_config_file)
@@ -80,18 +85,18 @@ class AuthManager(object):
authenticators.append(authenticator)
self.authenticators = authenticators
def check_registration_allowed(self, email, password):
"""Checks if the provided email is allowed to register."""
def check_registration_allowed(self, login, password):
"""Checks if the provided email/username is allowed to register."""
message = ''
status = 'done'
for provider, options in self.active_authenticators(email, password):
for provider, options in self.active_authenticators(login, password):
allow_reg = _get_tri_state(options, 'allow-register', True)
if allow_reg is None: # i.e. challenge
auth_result, msg = provider.authenticate(email, password, options)
auth_result, msg = provider.authenticate(login, password, options)
if auth_result is True:
break
if auth_result is None:
message = 'Invalid email address or password'
message = 'Invalid email address/username or password'
status = 'error'
break
elif allow_reg is True:
@@ -102,16 +107,17 @@ class AuthManager(object):
break
return message, status
def check_auto_registration(self, trans, email, password):
def check_auto_registration(self, trans, login, password):
"""
Checks the email/password using auth providers in order. If a match is
found, returns the 'auto-register' option for that provider.
Checks the username/email & password using auth providers in order.
If a match is found, returns the 'auto-register' option for that provider.
"""
for provider, options in self.active_authenticators(email, password):
for provider, options in self.active_authenticators(login, password):
if provider is None:
log.debug( "Unable to find module: %s" % options )
else:
auth_result, auto_username = provider.authenticate(email, password, options)
auth_result, auto_email, auto_username = provider.authenticate(login, password, options)
auto_email = str(auto_email).lower()
auto_username = str(auto_username).lower()
if auth_result is True:
# make username unique
@@ -124,16 +130,16 @@ class AuthManager(object):
i += 1
else:
break # end for loop if we can't make a unique username
log.debug( "Email: %s, auto-register with username: %s" % (email, auto_username) )
return (_get_bool(options, 'auto-register', False), auto_username)
log.debug( "Email: %s, auto-register with username: %s" % (auto_email, auto_username) )
return (_get_bool(options, 'auto-register', False), auto_email, auto_username)
elif auth_result is None:
log.debug( "Email: %s, stopping due to failed non-continue" % (email) )
log.debug( "Email: %s, Username %s, stopping due to failed non-continue" % (auto_email, auto_username) )
break # end authentication (skip rest)
return (False, '')
def check_password(self, user, password):
"""Checks the email/password using auth providers."""
for provider, options in self.active_authenticators(user.email, password):
"""Checks the username/email and password using auth providers."""
for provider, options in self.active_authenticators(user, password):
if provider is None:
log.debug( "Unable to find module: %s" % options )
else:
@@ -148,7 +154,7 @@ class AuthManager(object):
"""Checks that auth provider allows password changes and current_password
matches.
"""
for provider, options in self.active_authenticators(user.email, current_password):
for provider, options in self.active_authenticators(user, current_password):
if provider is None:
log.debug( "Unable to find module: %s" % options )
else:
@@ -162,7 +168,7 @@ class AuthManager(object):
return (False, 'Password change not supported')
return (False, 'Invalid current password')
def active_authenticators(self, username, password):
def active_authenticators(self, login, password):
"""Yields AuthProvider instances for the provided configfile that match the
filters.
"""
@@ -170,7 +176,7 @@ class AuthManager(object):
for authenticator in self.authenticators:
filter_template = authenticator.filter_template
if filter_template:
filter_str = filter_template.format(username=username, password=password)
filter_str = filter_template.format(login=login, password=password)
passed_filter = eval(filter_str, {"__builtins__": None}, {'str': str})
if not passed_filter:
continue # skip to next
+7 -7
View File
@@ -16,23 +16,23 @@ class AuthProvider(object):
""" Short string providing labelling this plugin """
@abc.abstractmethod
def authenticate(self, username, password, options):
def authenticate(self, login, password, options):
"""
Check that the username and password are correct.
NOTE: Used within auto-registration to check it is ok to register this
user.
:param username: the users email address
:type username: str
:param login: the user's email address or username
:type login: str
:param password: the plain text password they typed
:type password: str
:param options: options provided in auth_config_file
:type options: dict
:returns: True: accept user, False: reject user and None: reject user
and don't try any other providers. str is the username to register
with if accepting
:rtype: (bool, str)
and don't try any other providers. str, str is the email and
username to register with if accepting
:rtype: (bool, str, str)
"""
@abc.abstractmethod
@@ -44,7 +44,7 @@ class AuthProvider(object):
NOTE: used on normal login to check authentication and update user
details if required.
:param username: the users email address
:param username: the user's email address or username
:type username: str
:param password: the plain text password they typed
:type password: str
+2 -2
View File
@@ -16,11 +16,11 @@ class AlwaysReject(AuthProvider):
"""
plugin_type = 'alwaysreject'
def authenticate(self, username, password, options):
def authenticate(self, login, password, options):
"""
See abstract method documentation.
"""
return (None, '')
return (None, '', '')
def authenticate_user(self, user, password, options):
"""
@@ -4,34 +4,37 @@ Created on 15/07/2014
@author: Andrew Robinson
"""
import logging
from galaxy.exceptions import ConfigurationError
from ..providers import AuthProvider
from galaxy.auth import _get_bool
import logging
log = logging.getLogger(__name__)
def _get_subs(d, k, params):
if k not in d:
raise ConfigurationError("Missing '%s' parameter in Active Directory options" % k)
raise ConfigurationError("Missing '%s' parameter in LDAP options" % k)
return str(d[k]).format(**params)
class ActiveDirectory(AuthProvider):
"""
Attempts to authenticate users against an Active Directory server.
class LDAP(AuthProvider):
If options include search-fields then it will attempt to search the AD for
those fields first. After that it will bind to the AD with the username
"""
Attempts to authenticate users against an LDAP server.
If options include search-fields then it will attempt to search LDAP for
those fields first. After that it will bind to LDAP with the username
(formatted as specified).
"""
plugin_type = 'activedirectory'
plugin_type = 'ldap'
def authenticate(self, username, password, options):
def authenticate(self, login, password, options):
"""
See abstract method documentation.
"""
log.debug("Username: %s" % username)
log.debug("Login: %s" % login)
log.debug("Options: %s" % options)
failure_mode = False # reject but continue
@@ -41,11 +44,12 @@ class ActiveDirectory(AuthProvider):
try:
import ldap
except:
log.debug("User: %s, ACTIVEDIRECTORY: False (no ldap)" % (username))
log.debug(
"Login: %s, LDAP: False (could not load ldap module)" % (login))
return (failure_mode, '')
# do AD search (if required)
params = {'username': username, 'password': password}
# do LDAP search (if required)
params = {'login': login, 'password': password}
if 'search-fields' in options:
try:
# setup connection
@@ -54,21 +58,24 @@ class ActiveDirectory(AuthProvider):
l.protocol_version = 3
if 'search-user' in options:
l.simple_bind_s(_get_subs(options, 'search-user', params), _get_subs(options, 'search-password', params))
l.simple_bind_s(_get_subs(options, 'search-user', params),
_get_subs(options, 'search-password', params))
else:
l.simple_bind_s()
scope = ldap.SCOPE_SUBTREE
# setup search
attributes = [_.strip().format(**params) for _ in options['search-fields'].split(',')]
result = l.search(_get_subs(options, 'search-base', params), scope, _get_subs(options, 'search-filter', params), attributes)
attributes = [_.strip().format(**params)
for _ in options['search-fields'].split(',')]
result = l.search(_get_subs(options, 'search-base', params), scope,
_get_subs(options, 'search-filter', params), attributes)
# parse results
_, suser = l.result(result, 60)
dn, attrs = suser[0]
log.debug(("AD dn: %s" % dn))
log.debug(("AD Search attributes: %s" % attrs))
log.debug(("LDAP dn: %s" % dn))
log.debug(("LDAP Search attributes: %s" % attrs))
if hasattr(attrs, 'has_key'):
for attr in attributes:
if attr in attrs:
@@ -77,8 +84,8 @@ class ActiveDirectory(AuthProvider):
params[attr] = ""
params['dn'] = dn
except Exception:
log.exception('ACTIVEDIRECTORY Search Exception for User: %s' % username)
return (failure_mode, '')
log.exception('LDAP Search Exception for login: %s' % login)
return (failure_mode, '', '')
# end search
# bind as user to check their credentials
@@ -87,19 +94,31 @@ class ActiveDirectory(AuthProvider):
ldap.set_option(ldap.OPT_REFERRALS, 0)
l = ldap.initialize(_get_subs(options, 'server', params))
l.protocol_version = 3
l.simple_bind_s(_get_subs(options, 'bind-user', params), _get_subs(options, 'bind-password', params))
l.simple_bind_s(_get_subs(
options, 'bind-user', params), _get_subs(options, 'bind-password', params))
except Exception:
log.exception('ACTIVEDIRECTORY Authenticate Exception for User %s' % username)
return (failure_mode, '')
log.exception('LDAP Authentication Exception for login %s' % login)
return (failure_mode, '', '')
log.debug("User: %s, ACTIVEDIRECTORY: True" % (username))
return (True, _get_subs(options, 'auto-register-username', params))
log.debug("Login: %s, LDAP: True" % (login))
return (True,
_get_subs(options, 'auto-register-email', params),
_get_subs(options, 'auto-register-username', params))
def authenticate_user(self, user, password, options):
"""
See abstract method documentation.
"""
return self.authenticate(user.email, password, options)[0]
if _get_bool(options, 'login-use-username', False):
return self.authenticate(user.username, password, options)[0]
else:
return self.authenticate(user.email, password, options)[0]
__all__ = ['ActiveDirectory']
class ActiveDirectory(LDAP):
""" Effectively just an alias for LDAP auth, but may contain active directory specific
logic in the future. """
plugin_type = 'activedirectory'
__all__ = ['LDAP', 'ActiveDirectory']
+2 -2
View File
@@ -13,11 +13,11 @@ class LocalDB(AuthProvider):
"""Authenticate users against the local Galaxy database (as per usual)."""
plugin_type = 'localdb'
def authenticate(self, username, password, options):
def authenticate(self, login, password, options):
"""
See abstract method documentation.
"""
return (False, '') # it can never auto-create based of localdb (chicken-egg)
return (False, '', '') # it can never auto-create based of localdb (chicken-egg)
def authenticate_user(self, user, password, options):
"""
+5 -3
View File
@@ -37,7 +37,7 @@ def validate_email( trans, email, user=None, check_dup=True ):
def validate_publicname( trans, publicname, user=None ):
# User names must be at least four characters in length and contain only lower-case
# User names must be at least three characters in length and contain only lower-case
# letters, numbers, and the '-' character.
if user and user.username == publicname:
return ''
@@ -45,8 +45,10 @@ def validate_publicname( trans, publicname, user=None ):
if len( publicname ) < 3:
return "Public name must be at least 3 characters in length"
else:
if len( publicname ) < 4:
return "Public name must be at least 4 characters in length"
# DCT - TODO - Simplify logic if 3 chars is okay for publicname for
# galaxy as well as toolshed
if len( publicname ) < 3:
return "Public name must be at least 3 characters in length"
if len( publicname ) > 255:
return "Public name cannot be more than 255 characters in length"
if not( VALID_PUBLICNAME_RE.match( publicname ) ):
@@ -53,17 +53,21 @@ class Mobile( BaseUIController ):
# trans.log_event( "User logged out" )
# trans.handle_user_logout()
def __login( self, trans, email="", password="" ):
def __login(self, trans, login="", password=""):
return trans.response.send_redirect( web.url_for(controller='root', action='index' ) )
# error = password_error = None
# user = trans.sa_session.query( model.User ).filter_by( email = email ).first()
# user = trans.sa_session.query( model.User ).filter(or_(
# email == login,
# username == login
# )).first()
# if not user:
# autoreg = galaxy.auth.check_auto_registration(trans, email, password, trans.app.config.auth_config_file)
# autoreg = galaxy.auth.check_auto_registration(trans, login, password, trans.app.config.auth_config_file)
# if autoreg[0]:
# kwd = {}
# kwd['username'] = autoreg[1]
# kwd['email'] = autoreg[1]
# kwd['username'] = autoreg[2]
# params = util.Params( kwd )
# message = validate_email( trans, email )
# message = validate_email( trans, kwd['email'] )
# if not message:
# message, status, user, success = self.__register( trans, 'user', False, **kwd )
# if success:
+14 -12
View File
@@ -14,7 +14,7 @@ from datetime import datetime, timedelta
from galaxy import model
from galaxy import util
from galaxy import web
from galaxy.model.orm import and_
from galaxy.model.orm import and_, or_
from galaxy.security.validate_user_input import (transform_publicname,
validate_email,
validate_password,
@@ -467,7 +467,7 @@ class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, Creat
status = kwd.get( 'status', 'done' )
header = ''
user = trans.user
email = kwd.get( 'email', '' )
login = kwd.get( 'login', '' )
if user:
# Already logged in.
redirect_url = redirect
@@ -496,7 +496,7 @@ class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, Creat
else:
header = REQUIRE_LOGIN_TEMPLATE % ( "Galaxy tool shed", "" )
return trans.fill_template( '/user/login.mako',
email=email,
login=login,
header=header,
use_panels=use_panels,
redirect_url=redirect_url,
@@ -510,21 +510,23 @@ class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, Creat
def __validate_login( self, trans, **kwd ):
"""Validates numerous cases that might happen during the login time."""
message = escape( kwd.get( 'message', '' ) )
status = kwd.get( 'status', 'error' )
email = kwd.get( 'email', '' )
login = kwd.get( 'login', '' )
password = kwd.get( 'password', '' )
username = kwd.get( 'username', '' )
redirect = kwd.get( 'redirect', trans.request.referer ).strip()
success = False
user = trans.sa_session.query( trans.app.model.User ).filter( trans.app.model.User.table.c.email == email ).first()
user = trans.sa_session.query( trans.app.model.User ).filter(or_(
trans.app.model.User.table.c.email == login,
trans.app.model.User.table.c.username == login
)).first()
log.debug("trans.app.config.auth_config_file: %s" % trans.app.config.auth_config_file)
if not user:
autoreg = trans.app.auth_manager.check_auto_registration(trans, email, password)
autoreg = trans.app.auth_manager.check_auto_registration(trans, login, password)
if autoreg[0]:
kwd['username'] = autoreg[1]
kwd['email'] = autoreg[1]
kwd['username'] = autoreg[2]
params = util.Params( kwd )
message = validate_email( trans, email ) #self.__validate( trans, params, email, password, password, username )
message = validate_email( trans, kwd['email'] ) #self.__validate( trans, params, email, password, password, username )
if not message:
message, status, user, success = self.__register( trans, 'user', False, **kwd )
if success:
@@ -552,11 +554,11 @@ class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, Creat
elif trans.app.config.user_activation_on and not user.active: # activation is ON and the user is INACTIVE
if ( trans.app.config.activation_grace_period != 0 ): # grace period is ON
if self.is_outside_grace_period( trans, user.create_time ): # User is outside the grace period. Login is disabled and he will have the activation email resent.
message, status = self.resend_verification_email( trans, email, username )
message, status = self.resend_verification_email( trans, user.email, user.username )
else: # User is within the grace period, let him log in.
message, success, status = self.proceed_login( trans, user, redirect )
else: # Grace period is off. Login is disabled and user will have the activation email resent.
message, status = self.resend_verification_email( trans, email, username )
message, status = self.resend_verification_email( trans, user.email, user.username )
else: # activation is OFF
message, success, status = self.proceed_login( trans, user, redirect )
return ( message, status, user, success )
+4 -4
View File
@@ -9,7 +9,7 @@
function validateString(test_string, type) {
var mail_re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
var username_re = /^[a-z0-9\-]{4,255}$/;
var username_re = /^[a-z0-9\-]{3,255}$/;
if (type === 'email') {
return mail_re.test(test_string);
} else if (type === 'username'){
@@ -47,7 +47,7 @@
$( '#login_info' ).bind( 'submit', function( e ) {
var error_text_email= 'Please enter your valid email address.';
var error_text_email_long= 'Email cannot be more than 255 characters in length.';
var error_text_username_characters = 'Public name must contain only lowercase letters, numbers and "-". It also has to be shorter than 255 characters but longer than 3.';
var error_text_username_characters = 'Public name must contain only lowercase letters, numbers and "-". It also has to be shorter than 255 characters but longer than 2.';
var email = $( '#email_input' ).val();
var name = $( '#name_input' ).val();
var validForm = true;
@@ -108,7 +108,7 @@
<input type="text" id="name_input" name="username" size="40" value="${username | h}"/>
<div class="toolParamHelp" style="clear: both;">
Your public name provides a means of identifying you publicly within this tool shed. Public
names must be at least four characters in length and contain only lower-case letters, numbers,
names must be at least three characters in length and contain only lower-case letters, numbers,
and the '-' character. You cannot change your public name after you have created a repository
in this tool shed.
</div>
@@ -117,7 +117,7 @@
<input type="text" id="name_input" name="username" size="40" value="${username | h}"/>
<div class="toolParamHelp" style="clear: both;">
Your public name is an optional identifier that will be used to generate addresses for information
you share publicly. Public names must be at least four characters in length and contain only lower-case
you share publicly. Public names must be at least three characters in length and contain only lower-case
letters, numbers, and the '-' character.
</div>
%endif
+2 -2
View File
@@ -80,8 +80,8 @@ def inherit(context):
<div class="toolFormTitle">Login</div>
<form name="login" id="login" action="${form_action}" method="post" >
<div class="form-row">
<label>Email address:</label>
<input type="text" name="email" value="${email | h}" size="40"/>
<label>Username / Email Address:</label>
<input type="text" name="login" value="${login | h}" size="40"/>
<input type="hidden" name="redirect" value="${redirect | h}" size="40"/>
</div>
<div class="form-row">
+3 -3
View File
@@ -96,7 +96,7 @@ def inherit(context):
var error_text_email= 'Please enter your valid email address';
var error_text_email_long= 'Email cannot be more than 255 characters in length';
var error_text_username_characters = 'Public name must contain only lowercase letters, numbers and "-". It also has to be shorter than 255 characters but longer than 3.';
var error_text_username_characters = 'Public name must contain only lowercase letters, numbers and "-". It also has to be shorter than 255 characters but longer than 2.';
var error_text_password_short = 'Please use a password of at least 6 characters';
var error_text_password_match = "Passwords don't match";
@@ -141,13 +141,13 @@ def inherit(context):
%if t.webapp.name == 'galaxy':
<div class="toolParamHelp" style="clear: both;">
Your public name is an identifier that will be used to generate addresses for information
you share publicly. Public names must be at least four characters in length and contain only lower-case
you share publicly. Public names must be at least three characters in length and contain only lower-case
letters, numbers, and the '-' character.
</div>
%else:
<div class="toolParamHelp" style="clear: both;">
Your public name provides a means of identifying you publicly within this tool shed. Public
names must be at least four characters in length and contain only lower-case letters, numbers,
names must be at least three characters in length and contain only lower-case letters, numbers,
and the '-' character. You cannot change your public name after you have created a repository
in this tool shed.
</div>