refactor repo authentication to allow pluggable authentifier to login with something else than a password
--- a/dbapi.py Tue Oct 13 08:48:00 2009 +0200
+++ b/dbapi.py Tue Oct 13 08:50:19 2009 +0200
@@ -111,20 +111,21 @@
except Exception, ex:
raise ConnectionError(str(ex))
-def repo_connect(repo, login, password, cnxprops=None):
+def repo_connect(repo, login, **kwargs):
"""Constructor to create a new connection to the CubicWeb repository.
Returns a Connection instance.
"""
- cnxprops = cnxprops or ConnectionProperties('inmemory')
- cnxid = repo.connect(unicode(login), password, cnxprops=cnxprops)
- cnx = Connection(repo, cnxid, cnxprops)
- if cnxprops.cnxtype == 'inmemory':
+ if not 'cnxprops' in kwargs:
+ kwargs['cnxprops'] = ConnectionProperties('inmemory')
+ cnxid = repo.connect(unicode(login), **kwargs)
+ cnx = Connection(repo, cnxid, kwargs['cnxprops'])
+ if kwargs['cnxprops'].cnxtype == 'inmemory':
cnx.vreg = repo.vreg
return cnx
-def connect(database=None, login=None, password=None, host=None, group=None,
- cnxprops=None, setvreg=True, mulcnx=True, initlog=True):
+def connect(database=None, login=None, host=None, group=None,
+ cnxprops=None, setvreg=True, mulcnx=True, initlog=True, **kwargs):
"""Constructor for creating a connection to the CubicWeb repository.
Returns a Connection object.
@@ -154,11 +155,11 @@
vreg.set_schema(schema)
else:
vreg = None
- cnx = repo_connect(repo, login, password, cnxprops)
+ cnx = repo_connect(repo, login, cnxprops=cnxprops, **kwargs)
cnx.vreg = vreg
return cnx
-def in_memory_cnx(config, login, password):
+def in_memory_cnx(config, login, **kwargs):
"""usefull method for testing and scripting to get a dbapi.Connection
object connected to an in-memory repository instance
"""
@@ -171,7 +172,7 @@
repo = get_repository('inmemory', config=config, vreg=vreg)
# connection to the CubicWeb repository
cnxprops = ConnectionProperties('inmemory')
- cnx = repo_connect(repo, login, password, cnxprops=cnxprops)
+ cnx = repo_connect(repo, login, cnxprops=cnxprops, **kwargs)
return repo, cnx
--- a/devtools/__init__.py Tue Oct 13 08:48:00 2009 +0200
+++ b/devtools/__init__.py Tue Oct 13 08:50:19 2009 +0200
@@ -204,7 +204,7 @@
raise ValueError('no initialization function for driver %r' % driver)
config._cubes = None # avoid assertion error
repo, cnx = in_memory_cnx(config, unicode(sources['admin']['login']),
- sources['admin']['password'] or 'xxx')
+ password=sources['admin']['password'] or 'xxx')
if driver == 'sqlite':
install_sqlite_patch(repo.querier)
return repo, cnx
--- a/devtools/livetest.py Tue Oct 13 08:48:00 2009 +0200
+++ b/devtools/livetest.py Tue Oct 13 08:50:19 2009 +0200
@@ -151,7 +151,7 @@
# build a config, and get a connection
self.config = LivetestConfiguration(self.cube, self.sourcefile)
_, user, passwd, _ = loadconf()
- self.repo, self.cnx = in_memory_cnx(self.config, user, passwd)
+ self.repo, self.cnx = in_memory_cnx(self.config, user, password=passwd)
self.setup_db(self.cnx)
def tearDown(self):
--- a/devtools/stresstester.py Tue Oct 13 08:48:00 2009 +0200
+++ b/devtools/stresstester.py Tue Oct 13 08:50:19 2009 +0200
@@ -156,7 +156,7 @@
# get local access to the repository
print "Creating repo", prof_file
repo = Repository(config, prof_file)
- cnxid = repo.connect(user, password)
+ cnxid = repo.connect(user, password=password)
# connection to the CubicWeb repository
repo_cnx = Connection(repo, cnxid)
repo_cursor = repo_cnx.cursor()
--- a/devtools/testlib.py Tue Oct 13 08:48:00 2009 +0200
+++ b/devtools/testlib.py Tue Oct 13 08:50:19 2009 +0200
@@ -295,14 +295,16 @@
self._orig_cnx.commit()
return user
- def login(self, login, password=None):
+ def login(self, login, **kwargs):
"""return a connection for the given login/password"""
if login == self.admlogin:
self.restore_connection()
else:
+ if not kwargs:
+ kwargs['password'] = str(login)
self.cnx = repo_connect(self.repo, unicode(login),
- password or str(login),
- ConnectionProperties('inmemory'))
+ cnxprops=ConnectionProperties('inmemory'),
+ **kwargs)
if login == self.vreg.config.anonymous_user()[0]:
self.cnx.anonymous_connection = True
return self.cnx
--- a/goa/appobjects/sessions.py Tue Oct 13 08:48:00 2009 +0200
+++ b/goa/appobjects/sessions.py Tue Oct 13 08:50:19 2009 +0200
@@ -57,7 +57,7 @@
clear_cache(req, 'cursor')
cnxprops = ConnectionProperties(self.vreg.config.repo_method,
close=False, log=False)
- cnx = repo_connect(self._repo, login, password, cnxprops=cnxprops)
+ cnx = repo_connect(self._repo, login, password=password, cnxprops=cnxprops)
self._init_cnx(cnx, login, password)
# associate the connection to the current request
req.set_connection(cnx)
--- a/server/__init__.py Tue Oct 13 08:48:00 2009 +0200
+++ b/server/__init__.py Tue Oct 13 08:50:19 2009 +0200
@@ -175,7 +175,7 @@
session.commit()
# reloging using the admin user
config._cubes = None # avoid assertion error
- repo, cnx = in_memory_cnx(config, login, pwd)
+ repo, cnx = in_memory_cnx(config, login, password=pwd)
assert len(repo.sources) == 1, repo.sources
handler = config.migration_handler(schema, interactive=False,
cnx=cnx, repo=repo)
--- a/server/repository.py Tue Oct 13 08:48:00 2009 +0200
+++ b/server/repository.py Tue Oct 13 08:50:19 2009 +0200
@@ -378,7 +378,7 @@
session.close()
return login
- def authenticate_user(self, session, login, password):
+ def authenticate_user(self, session, login, **kwargs):
"""validate login / password, raise AuthenticationError on failure
return associated CWUser instance on success
"""
@@ -387,7 +387,7 @@
for source in self.sources:
if source.support_entity('CWUser'):
try:
- eid = source.authenticate(session, login, password)
+ eid = source.authenticate(session, login, **kwargs)
break
except AuthenticationError:
continue
@@ -526,7 +526,7 @@
session.close()
return True
- def connect(self, login, password, cnxprops=None):
+ def connect(self, login, **kwargs):
"""open a connection for a given user
base_url may be needed to send mails
@@ -539,10 +539,10 @@
session = self.internal_session()
# try to get a user object
try:
- user = self.authenticate_user(session, login, password)
+ user = self.authenticate_user(session, login, **kwargs)
finally:
session.close()
- session = Session(user, self, cnxprops)
+ session = Session(user, self, kwargs.get('cnxprops'))
user._cw = user.cw_rset.req = session
user.clear_related_cache()
self._sessions[session.id] = session
--- a/server/serverctl.py Tue Oct 13 08:48:00 2009 +0200
+++ b/server/serverctl.py Tue Oct 13 08:50:19 2009 +0200
@@ -106,7 +106,7 @@
login, pwd = manager_userpasswd()
while True:
try:
- return in_memory_cnx(config, login, pwd)
+ return in_memory_cnx(config, login, password=pwd)
except AuthenticationError:
print '-> Error: wrong user/password.'
# reset cubes else we'll have an assertion error on next retry
--- a/server/sources/__init__.py Tue Oct 13 08:48:00 2009 +0200
+++ b/server/sources/__init__.py Tue Oct 13 08:50:19 2009 +0200
@@ -288,7 +288,7 @@
"""
pass
- def authenticate(self, session, login, password):
+ def authenticate(self, session, login, **kwargs):
"""if the source support CWUser entity type, it should implements
this method which should return CWUser eid for the given login/password
if this account is defined in this source and valid login / password is
--- a/server/sources/ldapuser.py Tue Oct 13 08:48:00 2009 +0200
+++ b/server/sources/ldapuser.py Tue Oct 13 08:50:19 2009 +0200
@@ -237,14 +237,15 @@
self._connect()
return ConnectionWrapper(self._conn)
- def authenticate(self, session, login, password):
+ def authenticate(self, session, login, password=None, **kwargs):
"""return CWUser eid for the given login/password if this account is
defined in this source, else raise `AuthenticationError`
two queries are needed since passwords are stored crypted, so we have
to fetch the salt first
"""
- assert login, 'no login!'
+ if password is None:
+ raise AuthenticationError()
searchfilter = [filter_format('(%s=%s)', (self.user_login_attr, login))]
searchfilter.extend([filter_format('(%s=%s)', ('objectClass', o))
for o in self.user_classes])
--- a/server/sources/native.py Tue Oct 13 08:48:00 2009 +0200
+++ b/server/sources/native.py Tue Oct 13 08:50:19 2009 +0200
@@ -94,11 +94,6 @@
"""adapter for source using the native cubicweb schema (see below)
"""
sqlgen_class = SQLGenerator
-
- passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
- auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
- _sols = ({'X': 'CWUser', 'P': 'Password'},)
-
options = (
('db-driver',
{'type' : 'string',
@@ -146,6 +141,7 @@
def __init__(self, repo, appschema, source_config, *args, **kwargs):
SQLAdapterMixIn.__init__(self, source_config)
+ self.authentifiers = [LoginPasswordAuthentifier(self)]
AbstractSource.__init__(self, repo, appschema, source_config,
*args, **kwargs)
# sql generator
@@ -180,6 +176,11 @@
# consuming, find another way
return SQLAdapterMixIn.get_connection(self)
+ def add_authentifier(self, authentifier):
+ self.authentifiers.append(authentifier)
+ authentifier.source = self
+ authentifier.set_schema(self.schema)
+
def reset_caches(self):
"""method called during test to reset potential source caches"""
self._cache = Cache(self.repo.config['rql-cache-size'])
@@ -230,10 +231,10 @@
# ISource interface #######################################################
- def compile_rql(self, rql):
+ def compile_rql(self, rql, sols):
rqlst = self.repo.vreg.rqlhelper.parse(rql)
rqlst.restricted_vars = ()
- rqlst.children[0].solutions = self._sols
+ rqlst.children[0].solutions = sols
self.repo.querier.sqlgen_annotate(rqlst)
set_qdata(self.schema.rschema, rqlst, ())
return rqlst
@@ -247,10 +248,8 @@
self._rql_sqlgen.schema = schema
except AttributeError:
pass # __init__
- if 'CWUser' in schema: # probably an empty schema if not true...
- # rql syntax trees used to authenticate users
- self._passwd_rqlst = self.compile_rql(self.passwd_rql)
- self._auth_rqlst = self.compile_rql(self.auth_rql)
+ for authentifier in self.authentifiers:
+ authentifier.set_schema(self.schema)
def support_entity(self, etype, write=False):
"""return true if the given entity's type is handled by this adapter
@@ -271,29 +270,16 @@
def may_cross_relation(self, rtype):
return True
- def authenticate(self, session, login, password):
- """return CWUser eid for the given login/password if this account is
- defined in this source, else raise `AuthenticationError`
-
- two queries are needed since passwords are stored crypted, so we have
- to fetch the salt first
+ def authenticate(self, session, login, **kwargs):
+ """return CWUser eid for the given login and other authentication
+ information found in kwargs, else raise `AuthenticationError`
"""
- args = {'login': login, 'pwd' : password}
- if password is not None:
- rset = self.syntax_tree_search(session, self._passwd_rqlst, args)
+ for authentifier in self.authentifiers:
try:
- pwd = rset[0][0]
- except IndexError:
- raise AuthenticationError('bad login')
- # passwords are stored using the Bytes type, so we get a StringIO
- if pwd is not None:
- args['pwd'] = crypt_password(password, pwd.getvalue()[:2])
- # get eid from login and (crypted) password
- rset = self.syntax_tree_search(session, self._auth_rqlst, args)
- try:
- return rset[0][0]
- except IndexError:
- raise AuthenticationError('bad password')
+ return authentifier.authenticate(session, login, **kwargs)
+ except AuthenticationError:
+ continue
+ raise AuthenticationError()
def syntax_tree_search(self, session, union, args=None, cachekey=None,
varmap=None):
@@ -640,3 +626,49 @@
result += 'GRANT ALL ON deleted_entities TO %s;\n' % user
result += 'GRANT ALL ON entities_id_seq TO %s;\n' % user
return result
+
+
+class BaseAuthentifier(object):
+
+ def __init__(self, source=None):
+ self.source = source
+
+ def set_schema(self, schema):
+ """set the instance'schema"""
+ pass
+
+class LoginPasswordAuthentifier(BaseAuthentifier):
+ passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
+ auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
+ _sols = ({'X': 'CWUser', 'P': 'Password'},)
+
+ def set_schema(self, schema):
+ """set the instance'schema"""
+ if 'CWUser' in schema: # probably an empty schema if not true...
+ # rql syntax trees used to authenticate users
+ self._passwd_rqlst = self.source.compile_rql(self.passwd_rql, self._sols)
+ self._auth_rqlst = self.source.compile_rql(self.auth_rql, self._sols)
+
+ def authenticate(self, session, login, password=None, **kwargs):
+ """return CWUser eid for the given login/password if this account is
+ defined in this source, else raise `AuthenticationError`
+
+ two queries are needed since passwords are stored crypted, so we have
+ to fetch the salt first
+ """
+ args = {'login': login, 'pwd' : password}
+ if password is not None:
+ rset = self.source.syntax_tree_search(session, self._passwd_rqlst, args)
+ try:
+ pwd = rset[0][0]
+ except IndexError:
+ raise AuthenticationError('bad login')
+ # passwords are stored using the Bytes type, so we get a StringIO
+ if pwd is not None:
+ args['pwd'] = crypt_password(password, pwd.getvalue()[:2])
+ # get eid from login and (crypted) password
+ rset = self.source.syntax_tree_search(session, self._auth_rqlst, args)
+ try:
+ return rset[0][0]
+ except IndexError:
+ raise AuthenticationError('bad password')
--- a/web/views/authentication.py Tue Oct 13 08:48:00 2009 +0200
+++ b/web/views/authentication.py Tue Oct 13 08:50:19 2009 +0200
@@ -81,13 +81,14 @@
cnxprops = ConnectionProperties(self.vreg.config.repo_method,
close=False, log=self.log_queries)
try:
- cnx = repo_connect(self.repo, login, password, cnxprops=cnxprops)
+ cnx = repo_connect(self.repo, login, password=password,
+ cnxprops=cnxprops)
except AuthenticationError:
req.set_message(req._('authentication failure'))
# restore an anonymous connection if possible
anonlogin, anonpassword = self.vreg.config.anonymous_user()
if anonlogin and anonlogin != login:
- cnx = repo_connect(self.repo, anonlogin, anonpassword,
+ cnx = repo_connect(self.repo, anonlogin, password=anonpassword,
cnxprops=cnxprops)
self._init_cnx(cnx, anonlogin, anonpassword)
else: