diff --git a/acct_mgr/admin.py b/acct_mgr/admin.py --- a/acct_mgr/admin.py +++ b/acct_mgr/admin.py @@ -60,6 +60,8 @@ self.config.save() self.config.set('account-manager', 'force_passwd_change', req.args.get('force_passwd_change')) + self.config.set('account-manager', 'persistent_sessions', + req.args.get('persistent_sessions')) self.config.save() @@ -82,7 +84,8 @@ ] sections = sorted(sections, key=lambda i: i['name']) data = {'sections': sections, - 'force_passwd_change': self.account_manager.force_passwd_change} + 'force_passwd_change': self.account_manager.force_passwd_change, + 'persistent_sessions': self.account_manager.persistent_sessions} return 'admin_accountsconfig.html', data def _do_users(self, req): diff --git a/acct_mgr/api.py b/acct_mgr/api.py --- a/acct_mgr/api.py +++ b/acct_mgr/api.py @@ -90,6 +90,13 @@ "password when it's reset.") verify_accounts = BoolOption('account-manager', 'verify_accounts', True, "Force user to verify their email address") + persistent_sessions = BoolOption('account-manager', 'persistent_sessions', + False, doc="Allow the user to be " + "remembered across sessions without " + "needing to re-authenticate. This is, " + "user checks a \"Remember Me\" checkbox " + "and, next time he visits the site, he'll " + "be remembered") # Public API @@ -109,17 +116,17 @@ return self.password_store.check_password(user, password) def delete_user(self, user): - db = self.env.get_db_cnx() - cursor = db.cursor() - # Delete session attributes - cursor.execute("DELETE FROM session_attribute where sid=%s", (user,)) - # Delete session - cursor.execute("DELETE FROM session where sid=%s", (user,)) - # Delete any custom permissions set for the user - cursor.execute("DELETE FROM permission where username=%s", (user,)) + db = self.env.get_db_cnx() + cursor = db.cursor() + # Delete session attributes + cursor.execute("DELETE FROM session_attribute where sid=%s", (user,)) + # Delete session + cursor.execute("DELETE FROM session where sid=%s", (user,)) + # Delete any custom permissions set for the user + cursor.execute("DELETE FROM permission where username=%s", (user,)) db.commit() db.close() - # Delete from password store + # Delete from password store self.log.debug('deleted user') if self.password_store.delete_user(user): self._notify('deleted', user) diff --git a/acct_mgr/templates/admin_accountsconfig.html b/acct_mgr/templates/admin_accountsconfig.html --- a/acct_mgr/templates/admin_accountsconfig.html +++ b/acct_mgr/templates/admin_accountsconfig.html @@ -39,6 +39,22 @@ No + +
+ diff --git a/acct_mgr/templates/login.html b/acct_mgr/templates/login.html --- a/acct_mgr/templates/login.html +++ b/acct_mgr/templates/login.html @@ -34,6 +34,10 @@ +diff --git a/acct_mgr/web_ui.py b/acct_mgr/web_ui.py --- a/acct_mgr/web_ui.py +++ b/acct_mgr/web_ui.py @@ -11,6 +11,7 @@ import random import string +import time from trac import perm, util from trac.core import * @@ -416,7 +417,7 @@ class LoginModule(auth.LoginModule): - implements(ITemplateProvider) + implements(ITemplateProvider, IRequestFilter) def authenticate(self, req): if req.method == 'POST' and req.path_info.startswith('/login'): @@ -430,17 +431,110 @@ if req.path_info.startswith('/login') and req.authname == 'anonymous': data = { 'referer': self._referer(req), - 'reset_password_enabled': AccountModule(self.env).reset_password_enabled + 'reset_password_enabled': \ + AccountModule(self.env).reset_password_enabled, + 'persistent_sessions': \ + AccountManager(self.env).persistent_sessions } if req.method == 'POST': data['login_error'] = 'Invalid username or password' return 'login.html', data, None return auth.LoginModule.process_request(self, req) + # IRequestFilter methods + def pre_process_request(self, req, handler): + if 'trac_auth_session' in req.incookie: + # Let's check for a matching IP, proxy'ed or not + remote_addr = req.get_header("X-Forwarded-For") or req.remote_addr + if not req.incookie['trac_auth_session'].value == remote_addr: + self.log.debug("Cookie IP does not match Remote address IP: " + "'%s'!='%s'; Killing Session", + req.incookie['trac_auth_session'].value, + remote_addr) + self._do_logout(req) + # Repeat request after logout in order for the user not to + # think he's logged in + req.redirect(req.path_info) + + + # Let's now check with the one we have on auth_cookie db table + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT ipnr from auth_cookie WHERE cookie=%s", + (req.incookie['trac_auth'].value,)) + row = cursor.fetchone() + if not req.incookie['trac_auth_session'].value == row[0]: + self.log.debug("Cookie IP does not match DB IP: '%s'!='%s'; " + "Killing Session", + req.incookie['trac_auth_session'].value, + row[0]) + self._do_logout(req) + # Repeat request after logout in order for the user not to + # think he's logged in + req.redirect(req.path_info) + # XXX: Would there be a way to know if a user authenticated using a + # cookie and not the login form, and, if he requested /prefs force him + # to re-authenticate? + return handler + + def post_process_request(self, req, template, data, content_type): + return (template, data, content_type) + + def _get_name_for_cookie(self, req, cookie): + name = auth.LoginModule._get_name_for_cookie(self, req, cookie) + if not AccountManager(self.env).persistent_sessions: + # Persistent sessions not enabled + return name + + self.env.log.debug('Updating auth cookie %s for user %s' % + (cookie.value, name)) + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute('UPDATE auth_cookie SET time=%s WHERE cookie=%s', + (int(time.time()), cookie.value)) + db.commit() + req.outcookie['trac_auth'] = cookie.value + req.outcookie['trac_auth']['path'] = self.env.href() + + if 'trac_auth_session' in req.incookie: + # Let's check for a matching IP, proxy'ed or not + remote_addr = req.get_header("X-Forwarded-For") or req.remote_addr + if req.incookie['trac_auth_session'].value == remote_addr: + req.outcookie['trac_auth']['expires'] = 86400 * 30 + return name + def _do_login(self, req): if not req.remote_user: req.redirect(self.env.abs_href()) - return auth.LoginModule._do_login(self, req) + res = auth.LoginModule._do_login(self, req) + if req.args.get('rememberme', '0') == '1': + req.outcookie['trac_auth']['expires'] = 86400 * 30 + req.outcookie['trac_auth_session'] = req.remote_addr + req.outcookie['trac_auth_session']['expires'] = 86400 * 30 + return res + + def _do_logout(self, req): + """Log the user out. + + Simply deletes the corresponding record from the auth_cookie table. + """ + if req.authname == 'anonymous': + # Not logged in + return + + # While deleting this cookie we also take the opportunity to delete + # cookies older than 30 days + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("DELETE FROM auth_cookie WHERE name=%s OR time < %s", + (req.authname, int(time.time()) - 86400 * 30)) + db.commit() + self._expire_cookie(req) + + # Expire the persistent session cookie + req.outcookie['trac_auth_session'] = '' + req.outcookie['trac_auth_session']['path'] = self.env.href() + req.outcookie['trac_auth_session']['expires'] = -10000 def _remote_user(self, req): user = req.args.get('user')