refactor repo authentication to allow pluggable authentifier to login with something else than a password
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 13 Oct 2009 08:50:19 +0200
changeset 3647 2941f4a0aab9
parent 3646 3bba270202ef
child 3648 665c37544060
refactor repo authentication to allow pluggable authentifier to login with something else than a password
dbapi.py
devtools/__init__.py
devtools/livetest.py
devtools/stresstester.py
devtools/testlib.py
goa/appobjects/sessions.py
server/__init__.py
server/repository.py
server/serverctl.py
server/sources/__init__.py
server/sources/ldapuser.py
server/sources/native.py
web/views/authentication.py
--- 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: