# HG changeset patch # User Sylvain Thénault # Date 1255416619 -7200 # Node ID 2941f4a0aab9505855a6e3063249538c1fb97b69 # Parent 3bba270202ef2036f2cd0463d8da238c51a69926 refactor repo authentication to allow pluggable authentifier to login with something else than a password diff -r 3bba270202ef -r 2941f4a0aab9 dbapi.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 diff -r 3bba270202ef -r 2941f4a0aab9 devtools/__init__.py --- 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 diff -r 3bba270202ef -r 2941f4a0aab9 devtools/livetest.py --- 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): diff -r 3bba270202ef -r 2941f4a0aab9 devtools/stresstester.py --- 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() diff -r 3bba270202ef -r 2941f4a0aab9 devtools/testlib.py --- 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 diff -r 3bba270202ef -r 2941f4a0aab9 goa/appobjects/sessions.py --- 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) diff -r 3bba270202ef -r 2941f4a0aab9 server/__init__.py --- 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) diff -r 3bba270202ef -r 2941f4a0aab9 server/repository.py --- 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 diff -r 3bba270202ef -r 2941f4a0aab9 server/serverctl.py --- 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 diff -r 3bba270202ef -r 2941f4a0aab9 server/sources/__init__.py --- 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 diff -r 3bba270202ef -r 2941f4a0aab9 server/sources/ldapuser.py --- 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]) diff -r 3bba270202ef -r 2941f4a0aab9 server/sources/native.py --- 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') diff -r 3bba270202ef -r 2941f4a0aab9 web/views/authentication.py --- 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: