CVE-2022-3362: Insufficient Session Expiration (CWE-613)
value: Insufficient Session Expiration in GitHub repository ikus060/rdiffweb prior to 2.5.0.
CWE description: According to WASC, "Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization."
CVSS v3 base score: 9.8
Published: 2022-11-14T21:15Z
Repository: https://github.com/ikus060/rdiffweb
Language: Python
Files changed: README.md, rdiffweb/controller/api.py, rdiffweb/controller/page_admin_session.py, rdiffweb/controller/page_admin_users.py, rdiffweb/controller/page_delete.py, rdiffweb/controller/page_locations.py, rdiffweb/controller/page_pref_general.py, rdiffweb/controller/page_pref_mfa.py, rdiffweb/controller/page_pref_notification.py, rdiffweb/controller/page_pref_session.py, rdiffweb/controller/page_pref_sshkeys.py, rdiffweb/controller/page_pref_tokens.py, rdiffweb/controller/tests/test_api.py, rdiffweb/controller/tests/test_controller.py, rdiffweb/controller/tests/test_page_admin.py, rdiffweb/controller/tests/test_page_admin_users.py, rdiffweb/controller/tests/test_page_browse.py, rdiffweb/controller/tests/test_page_delete.py, rdiffweb/controller/tests/test_page_graphs.py, rdiffweb/controller/tests/test_page_history.py, rdiffweb/controller/tests/test_page_login.py, rdiffweb/controller/tests/test_page_logs.py, rdiffweb/controller/tests/test_page_mfa.py, rdiffweb/controller/tests/test_page_prefs_general.py, rdiffweb/controller/tests/test_page_prefs_mfa.py, rdiffweb/controller/tests/test_page_prefs_ssh.py, rdiffweb/controller/tests/test_page_prefs_tokens.py, rdiffweb/controller/tests/test_page_restore.py, rdiffweb/controller/tests/test_page_settings.py, rdiffweb/controller/tests/test_page_settings_remove_older.py, rdiffweb/controller/tests/test_page_settings_set_encoding.py, rdiffweb/controller/tests/test_page_status.py, rdiffweb/core/login.py, rdiffweb/core/model/__init__.py, rdiffweb/core/model/_repo.py, rdiffweb/core/model/_session.py, rdiffweb/core/model/_token.py, rdiffweb/core/model/_user.py, rdiffweb/core/model/tests/test_repo.py, rdiffweb/core/model/tests/test_token.py, rdiffweb/core/model/tests/test_user.py, rdiffweb/core/tests/test_login.py, rdiffweb/core/tests/test_notification.py, rdiffweb/core/tests/test_quota.py, rdiffweb/core/tests/test_rdw_templating.py, rdiffweb/rdw_app.py, rdiffweb/templates/access_token_added.html, rdiffweb/test.py, rdiffweb/tools/db.py
Commit date: 2022-11-02 09:55:21 -0400
Commit message: Delete user's session on password change

* Revisit add, delete commit function
* Clean-up Access Token using a scheduled job

Diff: diff --git a/README.md b/README.md
index ec5b4b1b6..fc6df3413 100644
--- a/README.md
+++ b/README.md
@@ -139,10 +139,11 @@ This next release focus on two-factor-authentication as a measure to increase se
 * Enforce 'Origin' validation [CVE-2022-3457](https://nvd.nist.gov/vuln/detail/CVE-2022-3457)
 * Define idle and absolute session timeout with agressive default to protect usage on public computer [CVE-2022-3327](https://nvd.nist.gov/vuln/detail/CVE-2022-3327)
 * Send email notification when enabling or disabling MFA [CVE-2022-3363](https://nvd.nist.gov/vuln/detail/CVE-2022-3363)
 * Use Argon2id to store password hash #231
 * Fixed plugin priorities to ensure that jobs are scheduled at each startup #232
+* Revoke previous user's sessions on password change [CVE-2022-3362](https://nvd.nist.gov/vuln/detail/CVE-2022-3362)
 
 Breaking changes:
 
 * Drop Ubuntu Hirsute & Impish (End-of-life)
 * `session-dir` is deprecated and should be replace by `rate-limit-dir`. User's session are stored in database.
diff --git a/rdiffweb/controller/api.py b/rdiffweb/controller/api.py
index 9b1e2bf6f..87dc2bca1 100644
--- a/rdiffweb/controller/api.py
+++ b/rdiffweb/controller/api.py
@@ -58,11 +58,14 @@ def _checkpassword(realm, username, password):
     """
     # Validate username
     userobj = UserObject.get_user(username)
     if userobj is not None:
         # Verify if the password matches a token.
-        if userobj.validate_access_token(password):
+        access_token = userobj.validate_access_token(password)
+        if access_token:
+            access_token.accessed()
+            access_token.commit()
             return True
         # Disable password authentication for MFA
         if userobj.mfa == UserObject.ENABLED_MFA:
             cherrypy.tools.ratelimit.hit()
             return False
@@ -76,11 +79,12 @@ def _checkpassword(realm, username, password):
 
 class ApiCurrentUser(Controller):
     @cherrypy.expose
     def default(self):
         u = self.app.currentuser
-        u.refresh_repos()
+        if u.refresh_repos():
+            u.commit()
         return {
             "email": u.email,
             "username": u.username,
             "repos": [
                 {
diff --git a/rdiffweb/controller/page_admin_session.py b/rdiffweb/controller/page_admin_session.py
index 065167ddf..a1747f449 100644
--- a/rdiffweb/controller/page_admin_session.py
+++ b/rdiffweb/controller/page_admin_session.py
@@ -44,10 +44,11 @@ class AdminSessionPage(Controller):
                     flash(_('The given session cannot be removed because it cannot be found.'), level='warning')
                 elif session.id == cherrypy.session.id:
                     flash(_('You cannot revoke your current session.'), level='warning')
                 else:
                     session.delete()
+                    session.commit()
                     flash(_('The session was successfully revoked.'), level='success')
             else:
                 flash(form.error_message, level='error')
         # Get list of current user's session
         obj_list = SessionObject.query.filter().all()
diff --git a/rdiffweb/controller/page_admin_users.py b/rdiffweb/controller/page_admin_users.py
index 668126f82..810ce423c 100644
--- a/rdiffweb/controller/page_admin_users.py
+++ b/rdiffweb/controller/page_admin_users.py
@@ -180,10 +180,11 @@ class UserForm(CherryForm):
         if old_quota != new_quota:
             userobj.disk_quota = new_quota
             # Setting quota will silently fail. Check if quota was updated.
             if userobj.disk_quota != new_quota:
                 flash(_("Setting user's quota is not supported"), level='warning')
+        userobj.commit()
 
 
 class EditUserForm(UserForm):
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
@@ -212,10 +213,11 @@ class AdminUsersPage(Controller):
         else:
             try:
                 user = UserObject.get_user(form.username.data)
                 if user:
                     user.delete()
+                    user.commit()
                     flash(_("User account removed."))
                 else:
                     flash(_("User doesn't exists!"), level='warning')
             except ValueError as e:
                 flash(e, level='error')
diff --git a/rdiffweb/controller/page_delete.py b/rdiffweb/controller/page_delete.py
index 3a79cca38..677a6c1ba 100644
--- a/rdiffweb/controller/page_delete.py
+++ b/rdiffweb/controller/page_delete.py
@@ -38,10 +38,15 @@ from rdiffweb.core.rdw_templating import url_for
 from rdiffweb.tools.i18n import gettext_lazy as _
 
 _logger = logging.getLogger(__name__)
 
 
+def delete_repo(repoobj, path):
+    repoobj.delete(path)
+    repoobj.commit()
+
+
 class DeleteRepoForm(CherryForm):
     confirm = StringField(_('Confirmation'), validators=[DataRequired()])
 
     def validate_confirm(self, field):
         if self.confirm.data != self.expected_confirm:
@@ -70,11 +75,11 @@ class DeletePage(Controller):
 
         form.expected_confirm = path_obj.display_name
         if form.is_submitted():
             if form.validate():
                 RepoObject.session.expunge(repo)
-                cherrypy.engine.publish('schedule_task', repo.delete, path)
+                cherrypy.engine.publish('schedule_task', delete_repo, repo, path)
                 # Redirect to parent folder or to root if repo get deleted
                 if path_obj.isroot:
                     raise cherrypy.HTTPRedirect(url_for('/'))
                 else:
                     parent_path = repo.fstat(os.path.dirname(path_obj.path))
diff --git a/rdiffweb/controller/page_locations.py b/rdiffweb/controller/page_locations.py
index 745704857..748b7aa5b 100644
--- a/rdiffweb/controller/page_locations.py
+++ b/rdiffweb/controller/page_locations.py
@@ -32,11 +32,12 @@ class LocationsPage(Controller):
     """
 
     @cherrypy.expose
     def index(self):
         # Get page params
-        self.app.currentuser.refresh_repos()
+        if self.app.currentuser.refresh_repos():
+            self.app.currentuser.commit()
         params = {
             "repos": self.app.currentuser.repo_objs,
             "disk_usage": self.app.currentuser.disk_usage,
             "disk_quota": self.app.currentuser.disk_quota,
         }
diff --git a/rdiffweb/controller/page_pref_general.py b/rdiffweb/controller/page_pref_general.py
index eb46eda68..9d51f34b1 100644
--- a/rdiffweb/controller/page_pref_general.py
+++ b/rdiffweb/controller/page_pref_general.py
@@ -56,11 +56,11 @@ class UserProfileForm(CherryForm):
         return super().is_submitted() and self.action.data == 'set_profile_info'
 
     def populate_obj(self, user):
         user.fullname = self.fullname.data
         user.email = self.email.data
-        user.add()
+        user.commit()
 
 
 class UserPasswordForm(CherryForm):
     action = HiddenField(default='set_password')
     current = PasswordField(
@@ -97,10 +97,11 @@ class UserPasswordForm(CherryForm):
         if not user.validate_password(self.current.data):
             self.current.errors = [_("Wrong current password.")]
             return False
         try:
             user.set_password(self.new.data)
+            user.commit()
             return True
         except ValueError as e:
             self.new.errors = [str(e)]
             return False
 
@@ -118,11 +119,12 @@ class RefreshForm(CherryForm):
         # Validate only if action is set_profile_info
         return super().is_submitted() and self.action.data == 'update_repos'
 
     def populate_obj(self, user):
         try:
-            user.refresh_repos(delete=True)
+            if user.refresh_repos(delete=True):
+                user.commit()
             flash(_("Repositories successfully updated"), level='success')
         except ValueError as e:
             flash(str(e), level='warning')
 
 
diff --git a/rdiffweb/controller/page_pref_mfa.py b/rdiffweb/controller/page_pref_mfa.py
index 918bf8d11..7b7602681 100644
--- a/rdiffweb/controller/page_pref_mfa.py
+++ b/rdiffweb/controller/page_pref_mfa.py
@@ -76,13 +76,15 @@ class MfaToggleForm(AbstractMfaForm):
 
     def populate_obj(self, userobj):
         # Enable or disable MFA only when a code is provided.
         if self.enable_mfa.data:
             userobj.mfa = UserObject.ENABLED_MFA
+            userobj.commit()
             flash(_("Two-Factor authentication enabled successfully."), level='success')
         elif self.disable_mfa.data:
             userobj.mfa = UserObject.DISABLED_MFA
+            userobj.commit()
             flash(_("Two-Factor authentication disabled successfully."), level='success')
 
     def validate_code(self, field):
         # Code is required for enable_mfa and disable_mfa
         if self.enable_mfa.data or self.disable_mfa.data:
diff --git a/rdiffweb/controller/page_pref_notification.py b/rdiffweb/controller/page_pref_notification.py
index 1e2d8971f..5ae485b45 100644
--- a/rdiffweb/controller/page_pref_notification.py
+++ b/rdiffweb/controller/page_pref_notification.py
@@ -74,10 +74,11 @@ class NotificationForm(CherryForm):
         # Loop trough user repo and update max age.
         for repo in userobj.repo_objs:
             if repo.display_name in self:
                 # Update the maxage
                 repo.maxage = self[repo.display_name].data
+        userobj.commit()
 
 
 class PagePrefNotification(Controller):
     @cherrypy.expose
     def default(self, action=None, **kwargs):
diff --git a/rdiffweb/controller/page_pref_session.py b/rdiffweb/controller/page_pref_session.py
index 72094e26c..95e27f24b 100644
--- a/rdiffweb/controller/page_pref_session.py
+++ b/rdiffweb/controller/page_pref_session.py
@@ -45,10 +45,11 @@ class PagePrefSession(Controller):
                     flash(_('The given session cannot be removed because it cannot be found.'), level='warning')
                 elif session.id == cherrypy.session.id:
                     flash(_('You cannot revoke your current session.'), level='warning')
                 else:
                     session.delete()
+                    session.commit()
                     flash(_('The session was successfully revoked.'), level='success')
             else:
                 flash(form.error_message, level='error')
         # Get list of current user's session
         obj_list = SessionObject.query.filter(SessionObject.username == self.app.currentuser.username).all()
diff --git a/rdiffweb/controller/page_pref_sshkeys.py b/rdiffweb/controller/page_pref_sshkeys.py
index ee5dc95c4..26e704fda 100644
--- a/rdiffweb/controller/page_pref_sshkeys.py
+++ b/rdiffweb/controller/page_pref_sshkeys.py
@@ -70,11 +70,13 @@ class SshForm(CherryForm):
     )
 
     def populate_obj(self, userobj):
         try:
             userobj.add_authorizedkey(key=self.key.data, comment=self.title.data)
+            userobj.commit()
         except DuplicateSSHKeyError as e:
+            userobj.rollback()
             flash(str(e), level='error')
         except Exception:
             flash(_("Unknown error while adding the SSH Key"), level='error')
             _logger.warning("error adding ssh key", exc_info=1)
 
@@ -84,10 +86,11 @@ class DeleteSshForm(CherryForm):
 
     def populate_obj(self, userobj):
         is_maintainer()
         try:
             userobj.delete_authorizedkey(self.fingerprint.data)
+            userobj.commit()
         except Exception:
             flash(_("Unknown error while removing the SSH Key"), level='error')
             _logger.warning("error removing ssh key", exc_info=1)
 
 
diff --git a/rdiffweb/controller/page_pref_tokens.py b/rdiffweb/controller/page_pref_tokens.py
index f961587e4..74365b85d 100644
--- a/rdiffweb/controller/page_pref_tokens.py
+++ b/rdiffweb/controller/page_pref_tokens.py
@@ -60,10 +60,11 @@ class TokenForm(CherryForm):
         return super().is_submitted() and self.action.data == 'add_access_token'
 
     def populate_obj(self, userobj):
         try:
             token = userobj.add_access_token(self.name.data, self.expiration.data)
+            userobj.commit()
             flash(
                 _(
                     "Your new personal access token has been created.\n"
                     "Make sure to save it - you won't be able to access it again.\n"
                     "%s"
diff --git a/rdiffweb/controller/tests/test_api.py b/rdiffweb/controller/tests/test_api.py
index d7b0a846a..d22236899 100644
--- a/rdiffweb/controller/tests/test_api.py
+++ b/rdiffweb/controller/tests/test_api.py
@@ -99,20 +99,21 @@ class APITest(rdiffweb.test.WebCase):
 
     def test_auth_with_access_token(self):
         # Given a user with an access token
         userobj = UserObject.get_user(self.USERNAME)
         token = userobj.add_access_token('test').encode('ascii')
+        userobj.commit()
         # When using this token to authenticated with /api
         self.getPage('/api/', headers=[("Authorization", "Basic " + b64encode(b"admin:" + token).decode('ascii'))])
         # Then authentication is successful
         self.assertStatus('200 OK')
 
     def test_auth_failed_with_mfa_enabled(self):
         # Given a user with MFA enabled
         userobj = UserObject.get_user(self.USERNAME)
         userobj.mfa = UserObject.ENABLED_MFA
-        userobj.add()
+        userobj.commit()
         # When authenticating with /api/
         self.getPage('/api/', headers=self.headers)
         # Then access is refused
         self.assertStatus('401 Unauthorized')
 
diff --git a/rdiffweb/controller/tests/test_controller.py b/rdiffweb/controller/tests/test_controller.py
index 5d477eade..309f76c6f 100644
--- a/rdiffweb/controller/tests/test_controller.py
+++ b/rdiffweb/controller/tests/test_controller.py
@@ -176,11 +176,11 @@ class ControllerSession(rdiffweb.test.WebCase):
         self.assertStatus(303)
         self.assertEqual(1, len(SessionObject.query.all()))
         # When this session get old
         data = SessionObject.query.filter(SessionObject.id == self.session_id).first()
         data.expiration_time = datetime.datetime.now() - datetime.timedelta(seconds=1)
-        data.add()
+        data.commit()
         session = DbSession(id=self.session_id)
         # Then the session get deleted by clean_up process
         session.clean_up()
         # Then session is deleted
         data = SessionObject.query.filter(SessionObject.id == self.session_id).first()
diff --git a/rdiffweb/controller/tests/test_page_admin.py b/rdiffweb/controller/tests/test_page_admin.py
index bb01562f5..7fbac0400 100644
--- a/rdiffweb/controller/tests/test_page_admin.py
+++ b/rdiffweb/controller/tests/test_page_admin.py
@@ -21,11 +21,12 @@ from rdiffweb.core.model import UserObject
 
 class AdminPagesAsUser(rdiffweb.test.WebCase):
     def setUp(self):
         super().setUp()
         # Add test user
-        UserObject.add_user('test', 'test123')
+        userobj = UserObject.add_user('test', 'test123')
+        userobj.commit()
         self._login('test', 'test123')
 
     @parameterized.expand(
         [
             "/admin",
diff --git a/rdiffweb/controller/tests/test_page_admin_users.py b/rdiffweb/controller/tests/test_page_admin_users.py
index c32a88f44..b437d67cc 100644
--- a/rdiffweb/controller/tests/test_page_admin_users.py
+++ b/rdiffweb/controller/tests/test_page_admin_users.py
@@ -335,11 +335,12 @@ class AbstractAdminTest(rdiffweb.test.WebCase):
         self.assertStatus(200)
         self.assertInBody("can&#39;t delete admin user")
 
     def test_delete_user_method_get(self):
         # Given a user
-        UserObject.add_user('newuser')
+        user = UserObject.add_user('newuser')
+        user.commit()
         # When trying to delete this user using method GET
         self.getPage("/admin/users/?action=delete&username=newuser", method='GET')
         # Then page return without error
         self.assertStatus(200)
         # Then user is not deleted
@@ -368,11 +369,12 @@ class AbstractAdminTest(rdiffweb.test.WebCase):
 
     def test_edit_user_with_invalid_path(self):
         """
         Verify failure trying to update user with invalid path.
         """
-        UserObject.add_user('test1')
+        userobj = UserObject.add_user('test1')
+        userobj.commit()
         self._edit_user("test1", "test1@test.com", "pr3j5Dwi", "/var/invalid/", UserObject.USER_ROLE)
         self.assertNotInBody("User added successfully.")
         self.assertInBody("User&#39;s root directory /var/invalid/ is not accessible!")
 
     def test_list(self):
@@ -395,22 +397,22 @@ class AbstractAdminTest(rdiffweb.test.WebCase):
 
     def test_user_invalid_root(self):
         # Delete all user's
         for user in UserObject.query.all():
             if user.username != self.USERNAME:
-                user.delete()
+                user.delete().commit()
         # Change the user's root
         user = UserObject.get_user('admin')
         user.user_root = "/invalid"
-        user.add()
+        user.commit()
         self.getPage("/admin/users")
         self.assertInBody("Root directory not accessible!")
 
         # Query the page by default
         user = UserObject.get_user('admin')
         user.user_root = "/tmp/"
-        user.add()
+        user.commit()
         self.getPage("/admin/users")
         self.assertNotInBody("Root directory not accessible!")
 
     def test_get_quota(self):
         # Mock a quota.
diff --git a/rdiffweb/controller/tests/test_page_browse.py b/rdiffweb/controller/tests/test_page_browse.py
index 420256c95..dc41bc4b0 100644
--- a/rdiffweb/controller/tests/test_page_browse.py
+++ b/rdiffweb/controller/tests/test_page_browse.py
@@ -47,12 +47,12 @@ class BrowsePageTest(rdiffweb.test.WebCase):
         self.assertInBody(self.REPO)
         self.assertInBody('testcases')
 
     def test_locations_with_broken_tree(self):
         userobj = UserObject.get_user(self.USERNAME)
-        RepoObject(userid=userobj.userid, repopath='testcases/broker-repo').add()
-        RepoObject(userid=userobj.userid, repopath='testcases/testcases').add()
+        RepoObject(userid=userobj.userid, repopath='testcases/broker-repo').add().commit()
+        RepoObject(userid=userobj.userid, repopath='testcases/testcases').add().commit()
         self.getPage("/")
 
     def test_WithRelativePath(self):
         """
         Check if relative path are resolved.
@@ -227,11 +227,11 @@ class BrowsePageTest(rdiffweb.test.WebCase):
         """
         # Change the user setting to match single repo.
         user = UserObject.get_user(self.USERNAME)
         user.user_root = os.path.join(self.testcases, 'testcases')
         user.refresh_repos()
-        user.add()
+        user.commit()
         self.assertEqual(['', 'broker-repo', 'testcases'], [r.name for r in user.repo_objs])
         # Check if listing locations is working
         self.getPage('/')
         self.assertStatus('200 OK')
         self.assertInBody('testcases')
@@ -247,11 +247,11 @@ class BrowsePageTest(rdiffweb.test.WebCase):
     def test_browse_with_permissions(self):
         # Create an another user with admin right
         user_obj = UserObject.add_user('anotheruser', 'password')
         user_obj.user_root = self.testcases
         user_obj.refresh_repos()
-        user_obj.add()
+        user_obj.commit()
         self.getPage('/browse/admin')
         self.assertStatus('404 Not Found')
 
         # Browse other user's repos
         self.getPage('/browse/anotheruser')
@@ -263,12 +263,12 @@ class BrowsePageTest(rdiffweb.test.WebCase):
 
     def test_browse_without_permissions(self):
         # Remove admin role.
         admin = UserObject.get_user('admin')
         admin.role = UserObject.USER_ROLE
-        admin.add()
         admin.refresh_repos()
+        admin.commit()
 
         # Browse other user's repos
         self.getPage('/browse/anotheruser/testcases')
         self.assertStatus('403 Forbidden')
         self.getPage('/browse/anotheruser/testcases/Revisions/')
@@ -276,11 +276,11 @@ class BrowsePageTest(rdiffweb.test.WebCase):
 
     def test_browser_with_failed_repo(self):
         # Given a failed repo
         admin = UserObject.get_user('admin')
         admin.user_root = 'invalid'
-        admin.add()
+        admin.commit()
         # When querying the logs
         self._browse(self.USERNAME, self.REPO, '')
         # Then the page is return with an error message
         self.assertStatus(200)
         self.assertInBody('The repository cannot be found or is badly damaged.')
diff --git a/rdiffweb/controller/tests/test_page_delete.py b/rdiffweb/controller/tests/test_page_delete.py
index 2ed89eff0..8dc680ce4 100644
--- a/rdiffweb/controller/tests/test_page_delete.py
+++ b/rdiffweb/controller/tests/test_page_delete.py
@@ -156,10 +156,11 @@ class DeleteRepoTest(rdiffweb.test.WebCase):
     def test_delete_repo_as_admin(self):
         # Create a another user with admin right
         user_obj = UserObject.add_user('anotheruser', 'password')
         user_obj.user_root = self.testcases
         user_obj.refresh_repos()
+        user_obj.commit()
         self.assertEqual(['broker-repo', 'testcases'], [r.name for r in user_obj.repo_objs])
 
         self._delete('anotheruser', 'testcases', 'testcases')
         self.assertStatus(303)
         self.assertHeaderItemValue('Location', self.baseurl + '/')
@@ -176,10 +177,11 @@ class DeleteRepoTest(rdiffweb.test.WebCase):
         # Create a another user with maintainer right
         user_obj = UserObject.add_user('maintainer', 'password')
         user_obj.user_root = self.testcases
         user_obj.role = UserObject.MAINTAINER_ROLE
         user_obj.refresh_repos()
+        user_obj.commit()
         self.assertEqual(['broker-repo', 'testcases'], [r.name for r in user_obj.repo_objs])
 
         # Login as maintainer
         self._login('maintainer', 'password')
 
@@ -198,10 +200,11 @@ class DeleteRepoTest(rdiffweb.test.WebCase):
         # Create a another user with maintainer right
         user_obj = UserObject.add_user('user', 'password')
         user_obj.user_root = self.testcases
         user_obj.role = UserObject.USER_ROLE
         user_obj.refresh_repos()
+        user_obj.commit()
         self.assertEqual(['broker-repo', 'testcases'], [r.name for r in user_obj.repo_objs])
 
         # Login as maintainer
         self._login('user', 'password')
 
diff --git a/rdiffweb/controller/tests/test_page_graphs.py b/rdiffweb/controller/tests/test_page_graphs.py
index a81429287..7eae458e0 100644
--- a/rdiffweb/controller/tests/test_page_graphs.py
+++ b/rdiffweb/controller/tests/test_page_graphs.py
@@ -52,19 +52,20 @@ class SettingsTest(rdiffweb.test.WebCase):
     def test_as_another_user(self):
         # Create another user with admin right
         user_obj = UserObject.add_user('anotheruser', 'password')
         user_obj.user_root = self.testcases
         user_obj.refresh_repos()
+        user_obj.commit()
 
         self.getPage("/graphs/activities/anotheruser/testcases")
         self.assertStatus('200 OK')
         self.assertInBody("Activities")
 
         # Remove admin role
         admin = UserObject.get_user('admin')
         admin.role = UserObject.USER_ROLE
-        admin.add()
+        admin.commit()
 
         # Browse admin's repos
         self.getPage("/graphs/activities/anotheruser/testcases")
         self.assertStatus('403 Forbidden')
 
@@ -88,11 +89,11 @@ class SettingsTest(rdiffweb.test.WebCase):
 
     def test_browser_with_failed_repo(self):
         # Given a failed repo
         admin = UserObject.get_user('admin')
         admin.user_root = 'invalid'
-        admin.add()
+        admin.commit()
         # When querying the logs
         self.getPage("/graphs/activities/" + self.USERNAME + "/" + self.REPO + "/")
         # Then the page is return with an error message
         self.assertStatus(200)
         self.assertInBody('The repository cannot be found or is badly damaged.')
diff --git a/rdiffweb/controller/tests/test_page_history.py b/rdiffweb/controller/tests/test_page_history.py
index 51e9b074e..653ceb4d7 100644
--- a/rdiffweb/controller/tests/test_page_history.py
+++ b/rdiffweb/controller/tests/test_page_history.py
@@ -73,17 +73,18 @@ class HistoryPageTest(rdiffweb.test.WebCase):
     def test_as_another_user(self):
         # Create a nother user with admin right
         user_obj = UserObject.add_user('anotheruser', 'password')
         user_obj.user_root = self.testcases
         user_obj.refresh_repos()
+        user_obj.commit()
         self.getPage("/history/anotheruser/testcases")
         self.assertStatus('200 OK')
 
         # Remove admin right
         admin = UserObject.get_user('admin')
         admin.role = UserObject.USER_ROLE
-        admin.add()
+        admin.commit()
 
         # Browse admin's repos
         self.getPage("/history/anotheruser/testcases")
         self.assertStatus('403 Forbidden')
 
@@ -97,11 +98,11 @@ class HistoryPageTest(rdiffweb.test.WebCase):
 
     def test_browser_with_failed_repo(self):
         # Given a failed repo
         admin = UserObject.get_user('admin')
         admin.user_root = 'invalid'
-        admin.add()
+        admin.commit()
         # When querying the logs
         self.getPage("/history/" + self.USERNAME + "/" + self.REPO)
         # Then the page is return with an error message
         self.assertStatus(200)
         self.assertInBody('The repository cannot be found or is badly damaged.')
diff --git a/rdiffweb/controller/tests/test_page_login.py b/rdiffweb/controller/tests/test_page_login.py
index 1f096b42f..5aa89fc09 100644
--- a/rdiffweb/controller/tests/test_page_login.py
+++ b/rdiffweb/controller/tests/test_page_login.py
@@ -168,11 +168,12 @@ class LoginPageTest(rdiffweb.test.WebCase):
         self.assertHeaderItemValue('Location', self.baseurl + "/")
         self.getPage('/')
         self.assertStatus(200)
         self.assertInBody(self.USERNAME)
         # Given another user
-        UserObject.add_user('otheruser', password='password')
+        userobj = UserObject.add_user('otheruser', password='password')
+        userobj.commit()
         # When trying to re-authenticated with login page
         self.getPage('/login/', method='POST', body={'login': 'otheruser', 'password': 'password'})
         # Then user is still authenticated with previous user
         self.assertStatus(303)
         self.assertHeaderItemValue('Location', self.baseurl + "/")
diff --git a/rdiffweb/controller/tests/test_page_logs.py b/rdiffweb/controller/tests/test_page_logs.py
index bb8439443..546637000 100644
--- a/rdiffweb/controller/tests/test_page_logs.py
+++ b/rdiffweb/controller/tests/test_page_logs.py
@@ -98,11 +98,11 @@ class LogsPageTest(rdiffweb.test.WebCase):
 
     def test_browser_with_failed_repo(self):
         # Given a failed repo
         admin = UserObject.get_user('admin')
         admin.user_root = 'invalid'
-        admin.add()
+        admin.commit()
         # When querying the logs
         self._log(self.USERNAME, self.REPO)
         # Then the page is return with an error message
         self.assertStatus(200)
         self.assertInBody('The repository cannot be found or is badly damaged.')
diff --git a/rdiffweb/controller/tests/test_page_mfa.py b/rdiffweb/controller/tests/test_page_mfa.py
index 091c5294c..a84c8ab12 100644
--- a/rdiffweb/controller/tests/test_page_mfa.py
+++ b/rdiffweb/controller/tests/test_page_mfa.py
@@ -46,11 +46,11 @@ class MfaPageTest(rdiffweb.test.WebCase):
         super().setUp()
         # Enabled MFA for all test cases
         userobj = UserObject.get_user(self.USERNAME)
         userobj.mfa = UserObject.ENABLED_MFA
         userobj.email = 'admin@example.com'
-        userobj.add()
+        userobj.commit()
 
     def test_get_without_login(self):
         # Given an unauthenticated user
         self.getPage("/logout")
         self.assertStatus(303)
@@ -62,11 +62,11 @@ class MfaPageTest(rdiffweb.test.WebCase):
 
     def test_get_with_mfa_disabled(self):
         # Given an authenticated user with MFA Disable
         userobj = UserObject.get_user(self.USERNAME)
         userobj.mfa = UserObject.DISABLED_MFA
-        userobj.add()
+        userobj.commit()
         self.getPage("/")
         self.assertStatus(200)
         # When requesting /mfa/ page
         self.getPage("/mfa/")
         # Then user is redirected to root page
@@ -75,11 +75,11 @@ class MfaPageTest(rdiffweb.test.WebCase):
 
     def test_get_with_user_without_email(self):
         # Given an authenticated user without email.
         userobj = UserObject.get_user(self.USERNAME)
         userobj.email = ''
-        userobj.add()
+        userobj.commit()
         # When requesting /mfa/ page
         self.getPage("/mfa/")
         # Then user is redirected to root page
         self.assertStatus(200)
         self.assertInBody(
@@ -280,11 +280,11 @@ class MfaPageWithWelcomeMsgTest(rdiffweb.test.WebCase):
         super().setUp()
         # Enabled MFA for all test cases
         userobj = UserObject.get_user(self.USERNAME)
         userobj.mfa = UserObject.ENABLED_MFA
         userobj.email = 'admin@example.com'
-        userobj.add()
+        userobj.commit()
 
     def test_getpage_default(self):
         # Given a user with MFA enabled
         # When querying the mfa page
         self.getPage('/mfa/', headers=[("Accept-Language", "it")])
diff --git a/rdiffweb/controller/tests/test_page_prefs_general.py b/rdiffweb/controller/tests/test_page_prefs_general.py
index 940f60504..e7bba6669 100644
--- a/rdiffweb/controller/tests/test_page_prefs_general.py
+++ b/rdiffweb/controller/tests/test_page_prefs_general.py
@@ -24,11 +24,11 @@ from unittest.mock import MagicMock
 
 import cherrypy
 from parameterized import parameterized
 
 import rdiffweb.test
-from rdiffweb.core.model import RepoObject, UserObject
+from rdiffweb.core.model import RepoObject, SessionObject, UserObject
 
 
 class PagePrefGeneralTest(rdiffweb.test.WebCase):
 
     PREFS = "/prefs/general"
@@ -173,20 +173,29 @@ class PagePrefGeneralTest(rdiffweb.test.WebCase):
     def test_change_email_with_too_long(self):
         self._set_profile_info(("test1" * 50) + "@test.com")
         self.assertInBody("Email too long.")
 
     def test_change_password(self):
-        self.listener.user_password_changed.reset_mock()
+        # Given a user with 3 active sessions
+        self.cookies = None
+        self._login(self.USERNAME, self.PASSWORD)
+        self.cookies = None
+        self._login(self.USERNAME, self.PASSWORD)
+        self.assertEqual(3, SessionObject.query.count())
         # When udating user's password
+        self.listener.user_password_changed.reset_mock()
         self._set_password(self.PASSWORD, "pr3j5Dwi", "pr3j5Dwi")
         # Then user is redirect to same page
         self.assertStatus(303)
         # Then the page return success message.
         self.getPage(self.PREFS)
         self.assertInBody("Password updated successfully.")
         # Then a notification is raised
         self.listener.user_password_changed.assert_called_once()
+        # Then all users session get deleted except our own session.
+        self.assertEqual(1, SessionObject.query.count())
+        self.assertEqual(self.session_id, SessionObject.query.first().id)
 
     def test_change_password_with_wrong_confirmation(self):
         self._set_password(self.PASSWORD, "t", "a")
         self.assertInBody("The new password and its confirmation do not match.")
 
@@ -229,11 +238,11 @@ class PagePrefGeneralTest(rdiffweb.test.WebCase):
         self.assertStatus(404)
 
     def test_update_repos(self):
         # Given a user with invalid repositories
         userobj = UserObject.get_user(self.USERNAME)
-        RepoObject(userid=userobj.userid, repopath='invalid').add()
+        RepoObject(userid=userobj.userid, repopath='invalid').add().commit()
         self.assertEqual(['broker-repo', 'invalid', 'testcases'], sorted([r.name for r in userobj.repo_objs]))
         # When updating the repository list
         self.getPage(self.PREFS, method='POST', body={'action': 'update_repos'})
         self.assertStatus(200)
         # Then a success message is displayed
diff --git a/rdiffweb/controller/tests/test_page_prefs_mfa.py b/rdiffweb/controller/tests/test_page_prefs_mfa.py
index 97e8e5325..0204cb77a 100644
--- a/rdiffweb/controller/tests/test_page_prefs_mfa.py
+++ b/rdiffweb/controller/tests/test_page_prefs_mfa.py
@@ -31,11 +31,11 @@ class PagePrefMfaTest(rdiffweb.test.WebCase):
     def setUp(self):
         super().setUp()
         # Define email for all test
         userobj = UserObject.get_user(self.USERNAME)
         userobj.email = 'admin@example.com'
-        userobj.add()
+        userobj.commit()
         # Register a listener on email
         self.listener = MagicMock()
         cherrypy.engine.subscribe('queue_mail', self.listener.queue_email, priority=50)
 
     def tearDown(self):
@@ -44,11 +44,11 @@ class PagePrefMfaTest(rdiffweb.test.WebCase):
 
     def _set_mfa(self, mfa):
         # Define mfa for user
         userobj = UserObject.get_user(self.USERNAME)
         userobj.mfa = mfa
-        userobj.add()
+        userobj.commit()
         # Reset mock.
         self.listener.queue_email.reset_mock()
         # Leave to disable mfa
         if mfa == UserObject.DISABLED_MFA:
             return
@@ -140,11 +140,11 @@ class PagePrefMfaTest(rdiffweb.test.WebCase):
         # Define mfa for user
         self._set_mfa(initial_mfa)
         # Given a user without email requesting a code
         userobj = UserObject.get_user(self.USERNAME)
         userobj.email = ''
-        userobj.add()
+        userobj.commit()
         # When trying to enable or disable mfa
         self.getPage("/prefs/mfa", method='POST', body={action: '1'})
         # Then an error is return to the user
         self.assertStatus(200)
         self.assertInBody("To continue, you must set up an email address for your account.")
diff --git a/rdiffweb/controller/tests/test_page_prefs_ssh.py b/rdiffweb/controller/tests/test_page_prefs_ssh.py
index e2406b77f..005bea4f0 100644
--- a/rdiffweb/controller/tests/test_page_prefs_ssh.py
+++ b/rdiffweb/controller/tests/test_page_prefs_ssh.py
@@ -67,10 +67,11 @@ class PagePrefSshKeysTest(rdiffweb.test.WebCase):
     def test_add_duplicate(self):
         # Delete existing keys
         user = UserObject.get_user('admin')
         for key in user.authorizedkeys:
             user.delete_authorizedkey(key.fingerprint)
+        user.commit()
         self.assertEqual(0, len(list(user.authorizedkeys)))
 
         # Add a new key
         self._add_ssh_key(
             "test@mysshkey",
@@ -204,10 +205,11 @@ class ApiSshKeysTest(rdiffweb.test.WebCase):
             user.delete_authorizedkey(key.fingerprint)
         user.add_authorizedkey(
             key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSEN5VTn9MLituZvdYTZMbZEaMxe0UuU7BelxHkvxzSpVWtazrIBEc3KZjtVoK9F3+0kd26P4DzSQuPUl3yZDgyZZeXrF6p2GlEA7A3tPuOEsAQ9c0oTiDYktq5/Go8vD+XAZKLd//qmCWW1Jg4datkWchMKJzbHUgBrBH015FDbGvGDWYTfVyb8I9H+LQ0GmbTHsuTu63DhPODncMtWPuS9be/flb4EEojMIx5Vce0SNO9Eih38W7jTvNWxZb75k5yfPJxBULRnS5v/fPnDVVtD3JSGybSwKoMdsMX5iImAeNhqnvd8gBu1f0IycUQexTbJXk1rPiRcF13SjKrfXz ikus060@ikus060-t530",
             comment="test@mysshkey",
         )
+        user.commit()
         self.assertEqual(1, len(list(user.authorizedkeys)))
         # When deleting the ssh key
         self.getPage(
             '/api/currentuser/sshkeys/4d:42:8b:35:e5:55:71:f7:b3:0d:58:f9:b1:2c:9e:91',
             headers=self.headers,
@@ -226,10 +228,11 @@ class ApiSshKeysTest(rdiffweb.test.WebCase):
             user.delete_authorizedkey(key.fingerprint)
         user.add_authorizedkey(
             key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSEN5VTn9MLituZvdYTZMbZEaMxe0UuU7BelxHkvxzSpVWtazrIBEc3KZjtVoK9F3+0kd26P4DzSQuPUl3yZDgyZZeXrF6p2GlEA7A3tPuOEsAQ9c0oTiDYktq5/Go8vD+XAZKLd//qmCWW1Jg4datkWchMKJzbHUgBrBH015FDbGvGDWYTfVyb8I9H+LQ0GmbTHsuTu63DhPODncMtWPuS9be/flb4EEojMIx5Vce0SNO9Eih38W7jTvNWxZb75k5yfPJxBULRnS5v/fPnDVVtD3JSGybSwKoMdsMX5iImAeNhqnvd8gBu1f0IycUQexTbJXk1rPiRcF13SjKrfXz ikus060@ikus060-t530",
             comment="test@mysshkey",
         )
+        user.commit()
         self.assertEqual(1, len(list(user.authorizedkeys)))
         # When deleting the ssh key
         data = self.getJson(
             '/api/currentuser/sshkeys/4d:42:8b:35:e5:55:71:f7:b3:0d:58:f9:b1:2c:9e:91',
             headers=self.headers,
@@ -248,10 +251,11 @@ class ApiSshKeysTest(rdiffweb.test.WebCase):
             user.delete_authorizedkey(key.fingerprint)
         user.add_authorizedkey(
             key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSEN5VTn9MLituZvdYTZMbZEaMxe0UuU7BelxHkvxzSpVWtazrIBEc3KZjtVoK9F3+0kd26P4DzSQuPUl3yZDgyZZeXrF6p2GlEA7A3tPuOEsAQ9c0oTiDYktq5/Go8vD+XAZKLd//qmCWW1Jg4datkWchMKJzbHUgBrBH015FDbGvGDWYTfVyb8I9H+LQ0GmbTHsuTu63DhPODncMtWPuS9be/flb4EEojMIx5Vce0SNO9Eih38W7jTvNWxZb75k5yfPJxBULRnS5v/fPnDVVtD3JSGybSwKoMdsMX5iImAeNhqnvd8gBu1f0IycUQexTbJXk1rPiRcF13SjKrfXz ikus060@ikus060-t530",
             comment="test@mysshkey",
         )
+        user.commit()
         self.assertEqual(1, len(list(user.authorizedkeys)))
         # When deleting the ssh key
         self.getPage(
             '/api/currentuser/sshkeys/invalid',
             headers=self.headers,
@@ -267,10 +271,11 @@ class ApiSshKeysTest(rdiffweb.test.WebCase):
             user.delete_authorizedkey(key.fingerprint)
         user.add_authorizedkey(
             key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSEN5VTn9MLituZvdYTZMbZEaMxe0UuU7BelxHkvxzSpVWtazrIBEc3KZjtVoK9F3+0kd26P4DzSQuPUl3yZDgyZZeXrF6p2GlEA7A3tPuOEsAQ9c0oTiDYktq5/Go8vD+XAZKLd//qmCWW1Jg4datkWchMKJzbHUgBrBH015FDbGvGDWYTfVyb8I9H+LQ0GmbTHsuTu63DhPODncMtWPuS9be/flb4EEojMIx5Vce0SNO9Eih38W7jTvNWxZb75k5yfPJxBULRnS5v/fPnDVVtD3JSGybSwKoMdsMX5iImAeNhqnvd8gBu1f0IycUQexTbJXk1rPiRcF13SjKrfXz ikus060@ikus060-t530",
             comment="test@mysshkey",
         )
+        user.commit()
         self.assertEqual(1, len(list(user.authorizedkeys)))
         # When deleting the ssh key
         data = self.getJson(
             '/api/currentuser/sshkeys',
             headers=self.headers,
diff --git a/rdiffweb/controller/tests/test_page_prefs_tokens.py b/rdiffweb/controller/tests/test_page_prefs_tokens.py
index 5f7e24b1f..88fde3495 100644
--- a/rdiffweb/controller/tests/test_page_prefs_tokens.py
+++ b/rdiffweb/controller/tests/test_page_prefs_tokens.py
@@ -93,10 +93,11 @@ class PagePrefTokensTest(rdiffweb.test.WebCase):
 
     def test_delete_access_token(self):
         # Given an existing user with access_token
         userobj = UserObject.get_user(self.USERNAME)
         userobj.add_access_token('test-token-name')
+        userobj.commit()
         # When deleting access token
         self.getPage(
             "/prefs/tokens",
             method='POST',
             body={'action': 'delete_access_token', 'name': 'test-token-name'},
diff --git a/rdiffweb/controller/tests/test_page_restore.py b/rdiffweb/controller/tests/test_page_restore.py
index f5002239d..54362afa1 100644
--- a/rdiffweb/controller/tests/test_page_restore.py
+++ b/rdiffweb/controller/tests/test_page_restore.py
@@ -345,17 +345,18 @@ class RestoreTest(rdiffweb.test.WebCase):
     def test_as_another_user(self):
         # Create a nother user with admin right
         user_obj = UserObject.add_user('anotheruser', 'password')
         user_obj.user_root = self.testcases
         user_obj.refresh_repos()
+        user_obj.commit()
         self._restore("anotheruser", "testcases", "Fichier%20%40%20%3Croot%3E/", "1414921853")
         self.assertStatus('200 OK')
         self.assertInBody("Ajout d'info")
 
         # Remove admin right
         admin = UserObject.get_user('admin')
         admin.role = UserObject.USER_ROLE
-        admin.add()
+        admin.commit()
 
         # Browse admin's repos
         self._restore("anotheruser", "testcases", "Fichier%20%40%20%3Croot%3E/", "1414921853")
         self.assertStatus('403 Forbidden')
diff --git a/rdiffweb/controller/tests/test_page_settings.py b/rdiffweb/controller/tests/test_page_settings.py
index a314ea3a3..efca5e223 100644
--- a/rdiffweb/controller/tests/test_page_settings.py
+++ b/rdiffweb/controller/tests/test_page_settings.py
@@ -38,18 +38,19 @@ class SettingsTest(rdiffweb.test.WebCase):
     def test_as_another_user(self):
         # Create a nother user with admin right
         user_obj = UserObject.add_user('anotheruser', 'password')
         user_obj.user_root = self.testcases
         user_obj.refresh_repos()
+        user_obj.commit()
         self.getPage("/settings/anotheruser/testcases")
         self.assertInBody("Character encoding")
         self.assertStatus('200 OK')
 
         # Remove admin right
         admin = UserObject.get_user('admin')
         admin.role = UserObject.USER_ROLE
-        admin.add()
+        admin.commit()
 
         # Browse admin's repos
         self.getPage("/settings/anotheruser/testcases")
         self.assertStatus('403 Forbidden')
 
@@ -79,11 +80,11 @@ class SettingsTest(rdiffweb.test.WebCase):
 
     def test_browser_with_failed_repo(self):
         # Given a failed repo
         admin = UserObject.get_user('admin')
         admin.user_root = '/invalid/'
-        admin.add()
+        admin.commit()
         # When querying the logs
         self.getPage("/settings/" + self.USERNAME + "/" + self.REPO)
         # Then the page is return with an error message
         self.assertStatus(200)
         self.assertInBody('The repository cannot be found or is badly damaged.')
diff --git a/rdiffweb/controller/tests/test_page_settings_remove_older.py b/rdiffweb/controller/tests/test_page_settings_remove_older.py
index aa7447e59..b263b79ed 100644
--- a/rdiffweb/controller/tests/test_page_settings_remove_older.py
+++ b/rdiffweb/controller/tests/test_page_settings_remove_older.py
@@ -50,19 +50,20 @@ class RemoveOlderTest(rdiffweb.test.WebCase):
     def test_as_another_user(self):
         # Create another user with admin right
         user_obj = UserObject.add_user('anotheruser', 'password')
         user_obj.user_root = self.testcases
         user_obj.refresh_repos()
+        user_obj.commit()
         self._remove_older('anotheruser', 'testcases', '1')
         self.assertStatus('200 OK')
         repo = RepoObject.query.filter(RepoObject.user == user_obj, RepoObject.repopath == self.REPO).first()
         self.assertEqual(1, repo.keepdays)
 
         # Remove admin right
         admin = UserObject.get_user('admin')
         admin.role = UserObject.USER_ROLE
-        admin.add()
+        admin.commit()
 
         # Browse admin's repos
         self._remove_older('anotheruser', 'testcases', '2')
         self.assertStatus('403 Forbidden')
 
diff --git a/rdiffweb/controller/tests/test_page_settings_set_encoding.py b/rdiffweb/controller/tests/test_page_settings_set_encoding.py
index 850c3cb4f..0b98af23a 100644
--- a/rdiffweb/controller/tests/test_page_settings_set_encoding.py
+++ b/rdiffweb/controller/tests/test_page_settings_set_encoding.py
@@ -92,19 +92,20 @@ class SetEncodingTest(rdiffweb.test.WebCase):
     def test_as_another_user(self):
         # Create another user with admin right
         user_obj = UserObject.add_user('anotheruser', 'password')
         user_obj.user_root = self.testcases
         user_obj.refresh_repos()
+        user_obj.commit()
         self._set_encoding('anotheruser', 'testcases', 'cp1252')
         self.assertStatus('200 OK')
         repo = RepoObject.query.filter(RepoObject.user == user_obj, RepoObject.repopath == self.REPO).first()
         self.assertEqual('cp1252', repo.encoding)
 
         # Remove admin right
         admin = UserObject.get_user('admin')
         admin.role = UserObject.USER_ROLE
-        admin.add()
+        admin.commit()
 
         # Browse admin's repos
         self._set_encoding('anotheruser', 'testcases', 'utf-8')
         self.assertStatus('403 Forbidden')
 
diff --git a/rdiffweb/controller/tests/test_page_status.py b/rdiffweb/controller/tests/test_page_status.py
index f0b427933..af389c8d7 100644
--- a/rdiffweb/controller/tests/test_page_status.py
+++ b/rdiffweb/controller/tests/test_page_status.py
@@ -37,11 +37,11 @@ class StatusTest(rdiffweb.test.WebCase):
 
     def test_page_with_broken_repo(self):
         # Given a user's with broken repo
         userobj = UserObject.get_user('admin')
         userobj.user_root = '/invalid/'
-        userobj.add()
+        userobj.commit()
         # When browsing the status page
         self.getPage("/status/")
         # Then not error should be raised
         self.assertStatus(200)
         self.assertInBody('Backup Status')
diff --git a/rdiffweb/core/login.py b/rdiffweb/core/login.py
index aca9903fb..607c8e683 100644
--- a/rdiffweb/core/login.py
+++ b/rdiffweb/core/login.py
@@ -83,11 +83,11 @@ class LoginPlugin(SimplePlugin):
                     username=real_username,
                     fullname=fullname,
                     email=email,
                     role=default_role,
                     user_root=default_user_root,
-                ).add()
+                ).commit()
             except Exception:
                 logger.error('fail to create new user', exc_info=1)
         if userobj is None:
             # User doesn't exists in database
             return None
@@ -99,11 +99,11 @@ class LoginPlugin(SimplePlugin):
             dirty = True
         if email:
             userobj.email = email
             dirty = True
         if dirty:
-            userobj.add()
+            userobj.commit()
         self.bus.publish('user_login', userobj)
         return userobj
 
 
 cherrypy.login = LoginPlugin(cherrypy.engine)
diff --git a/rdiffweb/core/model/__init__.py b/rdiffweb/core/model/__init__.py
index 2f3243009..ea057b06c 100644
--- a/rdiffweb/core/model/__init__.py
+++ b/rdiffweb/core/model/__init__.py
@@ -87,14 +87,14 @@ def db_after_create(target, connection, **kw):
     # versions. Also rename '.' to ''
     result = RepoObject.query.all()
     for row in result:
         if row.repopath.startswith('/') or row.repopath.endswith('/'):
             row.repopath = row.repopath.strip('/')
-            row.add()
+            row.commit()
         if row.repopath == '.':
             row.repopath = ''
-            row.add()
+            row.commit()
     # Remove duplicates and nested repositories.
     result = RepoObject.query.order_by(RepoObject.userid, RepoObject.repopath).all()
     prev_repo = (None, None)
     for row in result:
         if prev_repo[0] == row.userid and (prev_repo[1] == row.repopath or row.repopath.startswith(prev_repo[1] + '/')):
diff --git a/rdiffweb/core/model/_repo.py b/rdiffweb/core/model/_repo.py
index 73964cc30..a09c0fe18 100644
--- a/rdiffweb/core/model/_repo.py
+++ b/rdiffweb/core/model/_repo.py
@@ -94,11 +94,12 @@ class RepoObject(Base, RdiffRepo):
             and_(UserObject.username == username, RepoObject.repopath == repopath)
         )
         record = query.first()
         # If the repo is not found but refresh is requested
         if refresh and not record:
-            as_user.refresh_repos()
+            if as_user.refresh_repos():
+                as_user.commit()
             record = query.first()
         # If repo is not found, raise an error
         if not record:
             raise DoesNotExistError(username, repopath)
         return record
@@ -174,11 +175,11 @@ class RepoObject(Base, RdiffRepo):
         logger.info("deleting repository %s", self)
         # Remove data from disk
         RdiffRepo.delete(self, path=path)
         # Remove entry from database after deleting files.
         # Otherwise, refresh will add this repo back.
-        super().delete()
+        return super().delete()
 
     @validates('encoding')
     def validate_encoding(self, key, value):
         codec = encodings.search_function(value.lower())
         if not codec:
diff --git a/rdiffweb/core/model/_session.py b/rdiffweb/core/model/_session.py
index 8e96683db..34e4cc391 100644
--- a/rdiffweb/core/model/_session.py
+++ b/rdiffweb/core/model/_session.py
@@ -69,20 +69,22 @@ class DbSession(Session):
         if not session:
             session = SessionObject(id=self.id)
         session.data = self._data
         session.data['_timeout'] = self.timeout
         session.expiration_time = expiration_time
-        session.add()
+        session.add().commit()
 
     def _delete(self):
         SessionObject.query.filter(SessionObject.id == self.id).delete()
+        SessionObject.session.commit()
 
     def clean_up(self):
         """Clean up expired sessions."""
         try:
             now = self.now()
             SessionObject.query.filter(SessionObject.expiration_time < now).delete()
+            SessionObject.session.commit()
         except Exception:
             logger.error('fail to clean-up sessions', exc_info=1)
         finally:
             cherrypy.tools.db.on_end_resource()
 
diff --git a/rdiffweb/core/model/_token.py b/rdiffweb/core/model/_token.py
index fc9593a0d..267fb0cb1 100644
--- a/rdiffweb/core/model/_token.py
+++ b/rdiffweb/core/model/_token.py
@@ -15,10 +15,11 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import datetime
 
 import cherrypy
+from cherrypy.process.plugins import SimplePlugin
 from sqlalchemy import Column, DateTime, Integer, String
 from sqlalchemy.orm import relationship
 from sqlalchemy.sql import func
 
 Base = cherrypy.tools.db.get_base()
@@ -41,5 +42,35 @@ class Token(Base):
     expiration_time = Column('ExpirationTime', DateTime, nullable=True)
 
     @property
     def is_expired(self):
         return self.expiration_time is not None and self.expiration_time <= datetime.datetime.now()
+
+    def accessed(self):
+        self.access_time = datetime.datetime.utcnow()
+
+
+class TokenCleanup(SimplePlugin):
+
+    execution_time = '23:00'
+
+    def start(self):
+        self.bus.log('Start Token Clean Up plugin')
+        self.bus.publish('schedule_job', self.execution_time, self.clean_up)
+
+    start.priority = 55
+
+    def stop(self):
+        self.bus.log('Stop Token Clean Up plugin')
+        self.bus.publish('unschedule_job', self.clean_up)
+
+    stop.priority = 45
+
+    def clean_up(self):
+        Token.query.filter(Token.expiration_time <= datetime.datetime.now()).delete()
+        Token.session.commit()
+
+
+cherrypy.token_cleanup = TokenCleanup(cherrypy.engine)
+cherrypy.token_cleanup.subscribe()
+
+cherrypy.config.namespaces['token_cleanup'] = lambda key, value: setattr(cherrypy.token_cleanup, key, value)
diff --git a/rdiffweb/core/model/_user.py b/rdiffweb/core/model/_user.py
index d3d94bf1e..1510be3cb 100644
--- a/rdiffweb/core/model/_user.py
+++ b/rdiffweb/core/model/_user.py
@@ -12,11 +12,10 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
-import datetime
 import logging
 import os
 import secrets
 import string
 
@@ -31,10 +30,11 @@ import rdiffweb.tools.db  # noqa
 from rdiffweb.core import authorizedkeys
 from rdiffweb.core.passwd import check_password, hash_password
 from rdiffweb.tools.i18n import ugettext as _
 
 from ._repo import RepoObject
+from ._session import SessionObject
 from ._sshkey import SshKey
 from ._token import Token
 
 logger = logging.getLogger(__name__)
 
@@ -125,10 +125,11 @@ class UserObject(Base):
         elif default_password:
             userobj.hash_password = hash_password(default_password)
         else:
             userobj.hash_password = hash_password('admin123')
         userobj.add()
+        return userobj
 
     @classmethod
     def add_user(cls, username, password=None, role=USER_ROLE, **attrs):
         """
         Used to add a new user with an optional password.
@@ -175,13 +176,12 @@ class UserObject(Base):
                 authorizedkeys.add(fh, key)
         else:
             # Also look in database.
             logger.info("add key [%s] to [%s] database", key, self.username)
             try:
-                SshKey(userid=self.userid, fingerprint=key.fingerprint, key=key.getvalue()).add()
+                SshKey(userid=self.userid, fingerprint=key.fingerprint, key=key.getvalue()).add().flush()
             except IntegrityError:
-                SshKey.session.rollback()
                 raise DuplicateSSHKeyError(
                     _("Duplicate key. This key already exists or is associated to another user.")
                 )
         cherrypy.engine.publish('user_attr_changed', self, {'authorizedkeys': True})
 
@@ -193,14 +193,14 @@ class UserObject(Base):
         assert length >= 8
         # Generate a random token
         token = ''.join(secrets.choice(string.ascii_lowercase) for i in range(length))
         # Store hash token
         try:
-            obj = Token(userid=self.userid, name=name, hash_token=hash_password(token), expiration_time=expiration_time)
-            obj.add()
+            Token(
+                userid=self.userid, name=name, hash_token=hash_password(token), expiration_time=expiration_time
+            ).add().flush()
         except IntegrityError:
-            Token.session.rollback()
             raise ValueError(_("Duplicate token name: %s") % name)
         cherrypy.engine.publish('access_token_added', self, name)
         return token
 
     def valid_user_root(self):
@@ -219,11 +219,11 @@ class UserObject(Base):
         # FIXME This should be deleted by cascade
         SshKey.query.filter(SshKey.userid == self.userid).delete()
         RepoObject.query.filter(RepoObject.userid == self.userid).delete()
         Token.query.filter(Token.userid == self.userid).delete()
         # Delete ourself
-        Base.delete(self)
+        return Base.delete(self)
 
     def delete_authorizedkey(self, fingerprint):
         """
         Remove the given key from the user. Remove the key from his
         `authorized_keys` file if it exists and from database database.
@@ -376,13 +376,21 @@ class UserObject(Base):
                 msg += ' ' + warning
             if suggestions:
                 msg += ' ' + ' '.join(suggestions)
             raise ValueError(msg)
 
-        logger.info("updating user password [%s]", self.username)
+        # Store password
+        logger.info("updating user password [%s] and revoke sessions", self.username)
         self.hash_password = hash_password(password)
 
+        # Revoke other session to force re-login
+        session_id = cherrypy.serving.session.id if getattr(cherrypy.serving, 'session', None) else None
+        SessionObject.query.filter(
+            SessionObject.username == self.username,
+            SessionObject.id != session_id,
+        ).delete()
+
     def __eq__(self, other):
         return type(self) == type(other) and inspect(self).key == inspect(other).key
 
     @validates('username')
     def validates_username(self, key, value):
@@ -393,18 +401,15 @@ class UserObject(Base):
     def validate_access_token(self, token):
         """
         Check if the given token matches.
         """
         for access_token in Token.query.all():
-            # If token expired. Let delete it.
             if access_token.is_expired:
-                access_token.delete()
                 continue
             if check_password(token, access_token.hash_token):
-                # When it matches, let update the record.
-                access_token.access_time = datetime.datetime.utcnow
-                return True
+                # When it matches, return the record.
+                return access_token
         return False
 
     def validate_password(self, password):
         return check_password(password, self.hash_password)
 
diff --git a/rdiffweb/core/model/tests/test_repo.py b/rdiffweb/core/model/tests/test_repo.py
index 201b77a6a..bc5661754 100644
--- a/rdiffweb/core/model/tests/test_repo.py
+++ b/rdiffweb/core/model/tests/test_repo.py
@@ -31,11 +31,11 @@ from rdiffweb.core.model import RepoObject, UserObject
 class RepoObjectTest(rdiffweb.test.WebCase):
     def test_update_remove_duplicates(self):
         # Given a database with duplicate path
         userobj = UserObject.get_user(self.USERNAME)
         self.assertEqual(['broker-repo', 'testcases'], sorted([r.name for r in userobj.repo_objs]))
-        RepoObject(userid=userobj.userid, repopath='/testcases').add()
+        RepoObject(userid=userobj.userid, repopath='/testcases').add().commit()
         self.assertEqual(['/testcases', 'broker-repo', 'testcases'], sorted([r.name for r in userobj.repo_objs]))
         # When creating database
         cherrypy.tools.db.create_all()
         # Then duplicates are removed
         userobj = UserObject.get_user(self.USERNAME)
@@ -44,11 +44,11 @@ class RepoObjectTest(rdiffweb.test.WebCase):
     def test_update_remove_nested(self):
         # Given a database with nested path
         userobj = UserObject.get_user(self.USERNAME)
         self.assertEqual(['broker-repo', 'testcases'], sorted([r.name for r in userobj.repo_objs]))
         RepoObject(userid=userobj.userid, repopath='testcases/home/admin/testcases').add()
-        RepoObject(userid=userobj.userid, repopath='/testcases/home/admin/data').add()
+        RepoObject(userid=userobj.userid, repopath='/testcases/home/admin/data').add().commit()
         self.assertEqual(
             ['/testcases/home/admin/data', 'broker-repo', 'testcases', 'testcases/home/admin/testcases'],
             sorted([r.name for r in userobj.repo_objs]),
         )
         # When creating database
@@ -59,11 +59,11 @@ class RepoObjectTest(rdiffweb.test.WebCase):
 
     def test_update_repos_remove_slash(self):
         # Given a user with a repository named "/testcases"
         userobj = UserObject.get_user(self.USERNAME)
         RepoObject.query.filter(RepoObject.userid == userobj.userid).delete()
-        RepoObject(userid=userobj.userid, repopath='/testcases').add()
+        RepoObject(userid=userobj.userid, repopath='/testcases').add().commit()
         self.assertEqual(['/testcases'], sorted([r.name for r in userobj.repo_objs]))
         # When updating the database schema
         cherrypy.tools.db.create_all()
         # Then the repository name stripped the "/"
         userobj = UserObject.get_user(self.USERNAME)
@@ -71,11 +71,11 @@ class RepoObjectTest(rdiffweb.test.WebCase):
 
     def test_get_repo(self):
         user = UserObject.add_user('bernie', 'my-password')
         user.user_root = self.testcases
         user.refresh_repos()
-
+        user.commit()
         # Get as bernie
         repo_obj = RepoObject.get_repo('bernie/testcases', user)
         self.assertEqual('testcases', repo_obj.name)
         self.assertEqual('bernie', repo_obj.owner)
 
@@ -92,25 +92,29 @@ class RepoObjectTest(rdiffweb.test.WebCase):
 
     def test_get_repo_as_other_user(self):
         user = UserObject.add_user('bernie', 'my-password')
         user.user_root = self.testcases
         user.refresh_repos()
+        user.commit()
         RepoObject.get_repo('bernie/testcases', user)
 
         # Get as otheruser
         other = UserObject.add_user('other')
+        other.commit()
         with self.assertRaises(AccessDeniedError):
             RepoObject.get_repo('bernie/testcases', other)
 
     def test_get_repo_as_admin(self):
         user = UserObject.add_user('bernie', 'my-password')
         user.user_root = self.testcases
         user.refresh_repos()
+        user.commit()
 
         # Get as admin
         other = UserObject.add_user('other')
         other.role = UserObject.ADMIN_ROLE
+        other.commit()
         repo_obj3 = RepoObject.get_repo('bernie/testcases', other)
         self.assertEqual('testcases', repo_obj3.name)
         self.assertEqual('bernie', repo_obj3.owner)
 
     def test_str(self):
@@ -126,33 +130,33 @@ class RepoObjectTest(rdiffweb.test.WebCase):
 
     def test_set_get_encoding(self):
         userobj = UserObject.get_user(self.USERNAME)
         repo_obj = RepoObject.query.filter(RepoObject.user == userobj, RepoObject.repopath == self.REPO).first()
         repo_obj.encoding = "cp1252"
-        repo_obj.add()
+        repo_obj.commit()
         repo_obj = RepoObject.query.filter(RepoObject.user == userobj, RepoObject.repopath == self.REPO).first()
         self.assertEqual("cp1252", repo_obj.encoding)
         # Check with invalid value.
         with self.assertRaises(ValueError):
             repo_obj.encoding = "invalid"
 
     def test_set_get_maxage(self):
         userobj = UserObject.get_user(self.USERNAME)
         repo_obj = RepoObject.query.filter(RepoObject.user == userobj, RepoObject.repopath == self.REPO).first()
         repo_obj.maxage = 10
-        repo_obj.add()
+        repo_obj.commit()
         repo_obj = RepoObject.query.filter(RepoObject.user == userobj, RepoObject.repopath == self.REPO).first()
         self.assertEqual(10, repo_obj.maxage)
         # Check with invalid value.
         with self.assertRaises(ValueError):
             repo_obj.maxage = "invalid"
 
     def test_set_get_keepdays(self):
         userobj = UserObject.get_user(self.USERNAME)
         repo_obj = RepoObject.query.filter(RepoObject.user == userobj, RepoObject.repopath == self.REPO).first()
         repo_obj.keepdays = 10
-        repo_obj.add()
+        repo_obj.commit()
         repo_obj = RepoObject.query.filter(RepoObject.user == userobj, RepoObject.repopath == self.REPO).first()
         self.assertEqual(10, repo_obj.keepdays)
         # Check with invalid value.
         with self.assertRaises(ValueError):
             repo_obj.keepdays = "invalid"
@@ -167,20 +171,20 @@ class RepoObjectTest(rdiffweb.test.WebCase):
 
     def test_keepdays_default_value_from_init(self):
         # Given a User
         userobj = UserObject.get_user(self.USERNAME)
         # When creating a new repository
-        repo_obj = RepoObject(user=userobj, repopath='repopath').add()
+        repo_obj = RepoObject(user=userobj, repopath='repopath').add().commit()
         # New repo get created with keepdays == -1
         self.assertEqual('-1', repo_obj._keepdays)
         self.assertEqual(-1, repo_obj.keepdays)
 
     def test_keepdays_empty_string(self):
         # Given a User
         userobj = UserObject.get_user(self.USERNAME)
         # When creating a new repository
-        repo_obj = RepoObject(user=userobj, repopath='repopath').add()
+        repo_obj = RepoObject(user=userobj, repopath='repopath').add().commit()
         RepoObject.session.execute(
             RepoObject.__table__.update().where(RepoObject.__table__.c.RepoID == repo_obj.repoid).values(keepdays='')
         )
         RepoObject.session.commit()
         repo_obj.expire()
@@ -197,8 +201,8 @@ class RepoObjectTest(rdiffweb.test.WebCase):
 
     def test_encoding_default_value_from_init(self):
         # Given a User
         userobj = UserObject.get_user(self.USERNAME)
         # When creating a new repository
-        repo_obj = RepoObject(user=userobj, repopath='repopath').add()
+        repo_obj = RepoObject(user=userobj, repopath='repopath').add().commit()
         # New repo get created with utf-8
         self.assertEqual('utf-8', repo_obj.encoding)
diff --git a/rdiffweb/core/model/tests/test_token.py b/rdiffweb/core/model/tests/test_token.py
new file mode 100644
index 000000000..698a6fb26
--- /dev/null
+++ b/rdiffweb/core/model/tests/test_token.py
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+# rdiffweb, A web interface to rdiff-backup repositories
+# Copyright (C) 2012-2021 rdiffweb contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Created on June 30, 2022
+
+Module to test `user` model.
+
+@author: Patrik Dufresne <patrik@ikus-soft.com>
+"""
+import datetime
+from io import open
+from unittest.mock import MagicMock
+
+import cherrypy
+import pkg_resources
+
+import rdiffweb.test
+from rdiffweb.core.model import Token, UserObject
+
+
+class TokenTest(rdiffweb.test.WebCase):
+    def _read_ssh_key(self):
+        """Readthe pub key from test packages"""
+        filename = pkg_resources.resource_filename('rdiffweb.core.tests', 'test_publickey_ssh_rsa.pub')
+        with open(filename, 'r', encoding='utf8') as f:
+            return f.readline()
+
+    def _read_authorized_keys(self):
+        """Read the content of test_authorized_keys"""
+        filename = pkg_resources.resource_filename('rdiffweb.core.tests', 'test_authorized_keys')
+        with open(filename, 'r', encoding='utf8') as f:
+            return f.read()
+
+    def setUp(self):
+        super().setUp()
+        self.listener = MagicMock()
+        cherrypy.engine.subscribe('access_token_added', self.listener.access_token_added, priority=50)
+        cherrypy.engine.subscribe('queue_mail', self.listener.queue_mail, priority=50)
+
+    def tearDown(self):
+        cherrypy.engine.unsubscribe('access_token_added', self.listener.access_token_added)
+        cherrypy.engine.unsubscribe('queue_mail', self.listener.queue_mail)
+        return super().tearDown()
+
+    def test_check_schedule(self):
+        # Given the application is started
+        # Then remove_older job should be schedule
+        self.assertEqual(1, len([job for job in cherrypy.scheduler.list_jobs() if job.name == 'clean_up']))
+
+    def test_clean_up_without_expired(self):
+        # Given a user with 3 Token
+        user = UserObject.get_user(self.USERNAME)
+        user.add_access_token('test1')
+        user.add_access_token('test2')
+        user.add_access_token('test3')
+        user.commit()
+        self.assertEqual(3, Token.query.count())
+        # When running notification_job
+        cherrypy.token_cleanup.clean_up()
+        # Then token are not removed
+        self.assertEqual(3, Token.query.count())
+
+    def test_clean_up_with_expired(self):
+        # Given a user with 3 Token
+        user = UserObject.get_user(self.USERNAME)
+        user.add_access_token('test1')
+        user.add_access_token('test2')
+        user.add_access_token('test3')
+        for t in Token.query.all():
+            t.expiration_time = datetime.datetime.now()
+        user.commit()
+        self.assertEqual(3, Token.query.count())
+        # When running notification_job
+        cherrypy.token_cleanup.clean_up()
+        # Then token are not removed
+        self.assertEqual(0, Token.query.count())
+
+    def test_add_access_token(self):
+        # Given a user with an email
+        userobj = UserObject.get_user(self.USERNAME)
+        userobj.email = 'test@examples.com'
+        userobj.commit()
+        # When adding a new token
+        token = userobj.add_access_token('test')
+        userobj.commit()
+        # Then a new token get created
+        self.assertTrue(token)
+        tokenobj = Token.query.filter(Token.userid == userobj.userid).first()
+        self.assertTrue(tokenobj)
+        self.assertEqual(None, tokenobj.expiration_time)
+        self.assertEqual(None, tokenobj.access_time)
+        # Then an email is sent to the user.
+        self.listener.access_token_added.assert_called_once_with(userobj, 'test')
+        self.listener.queue_mail.assert_called_once()
+
+    def test_add_access_token_duplicate_name(self):
+        # Given a user with an existing token
+        userobj = UserObject.get_user(self.USERNAME)
+        userobj.add_access_token('test')
+        userobj.commit()
+        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
+        # When adding a new token with the same name
+        with self.assertRaises(ValueError):
+            userobj.add_access_token('test')
+            userobj.commit()
+        userobj.rollback()
+        # Then token is not created
+        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
+        # Then an email is not sent.
+        self.listener.access_token_added.assert_called_once_with(userobj, 'test')
+
+    def test_delete_access_token(self):
+        # Given a user with an existing token
+        userobj = UserObject.get_user(self.USERNAME)
+        userobj.add_access_token('test')
+        userobj.commit()
+        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
+        # When deleting an access token
+        userobj.delete_access_token('test')
+        userobj.commit()
+        # Then Token get deleted
+        self.assertEqual(0, Token.query.filter(Token.userid == userobj.userid).count())
+
+    def test_delete_access_token_invalid(self):
+        # Given a user with an existing token
+        userobj = UserObject.get_user(self.USERNAME)
+        userobj.add_access_token('test')
+        userobj.commit()
+        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
+        # When deleting an invalid access token
+        with self.assertRaises(ValueError):
+            userobj.delete_access_token('invalid')
+        # Then Token not deleted
+        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
+
+    def test_delete_user_remove_access_tokens(self):
+        # Given a user with an existing token
+        userobj = UserObject.add_user('testuser', 'password')
+        userobj.commit()
+        userobj.add_access_token('test')
+        userobj.commit()
+        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
+        # When deleting the user
+        userobj.delete()
+        userobj.commit()
+        # Then Token get deleted
+        self.assertEqual(0, Token.query.filter(Token.userid == userobj.userid).count())
+
+    def test_verify_access_token(self):
+        # Given a user with an existing token
+        userobj = UserObject.get_user(self.USERNAME)
+        token = userobj.add_access_token('test')
+        userobj.commit()
+        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
+        # When validating the token
+        # Then token is valid
+        self.assertTrue(userobj.validate_access_token(token))
+
+    def test_verify_access_token_with_expired(self):
+        # Given a user with an existing token
+        userobj = UserObject.get_user(self.USERNAME)
+        token = userobj.add_access_token(
+            'test', expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1)
+        )
+        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
+        # When validating the token
+        # Then token is invalid
+        self.assertFalse(userobj.validate_access_token(token))
+        # Then token is not removed
+        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
+
+    def test_verify_access_token_with_invalid(self):
+        # Given a user with an existing token
+        userobj = UserObject.get_user(self.USERNAME)
+        userobj.add_access_token('test', expiration_time=datetime.datetime.now())
+        userobj.commit()
+        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
+        # When validating the token
+        # Then token is invalid
+        self.assertFalse(userobj.validate_access_token('invalid'))
diff --git a/rdiffweb/core/model/tests/test_user.py b/rdiffweb/core/model/tests/test_user.py
index 4a57ded52..497e0738c 100644
--- a/rdiffweb/core/model/tests/test_user.py
+++ b/rdiffweb/core/model/tests/test_user.py
@@ -19,22 +19,21 @@ Created on June 30, 2022
 
 Module to test `user` model.
 
 @author: Patrik Dufresne <patrik@ikus-soft.com>
 """
-import datetime
 import os
 from io import StringIO, open
 from unittest.mock import MagicMock
 
 import cherrypy
 import pkg_resources
 from parameterized import parameterized
 
 import rdiffweb.test
 from rdiffweb.core import authorizedkeys
-from rdiffweb.core.model import DuplicateSSHKeyError, RepoObject, Token, UserObject
+from rdiffweb.core.model import DuplicateSSHKeyError, RepoObject, UserObject
 from rdiffweb.core.passwd import check_password
 
 
 class UserObjectTest(rdiffweb.test.WebCase):
     def _read_ssh_key(self):
@@ -71,11 +70,11 @@ class UserObjectTest(rdiffweb.test.WebCase):
         return super().tearDown()
 
     def test_add_user(self):
         """Add user to database."""
         userobj = UserObject.add_user('joe')
-        self.assertIsNotNone(userobj)
+        userobj.commit()
         self.assertIsNotNone(UserObject.get_user('joe'))
         # Check if listener called
         self.listener.user_added.assert_called_once_with(userobj)
 
     def test_add_user_updated_by_listener(self):
@@ -85,29 +84,31 @@ class UserObjectTest(rdiffweb.test.WebCase):
             userobj.user_root = '/new/value'
 
         self.listener.user_added.side_effect = change_user_obj
         # When adding user
         userobj = UserObject.add_user('joe')
-        self.assertIsNotNone(userobj)
+        userobj.commit()
         self.assertIsNotNone(UserObject.get_user('joe'))
         # Then lister get called
         self.listener.user_added.assert_called_once_with(userobj)
         # Then object was updated by listener
         self.assertEqual('/new/value', userobj.user_root)
 
     def test_add_user_with_duplicate(self):
         """Add user to database."""
-        UserObject.add_user('denise')
+        user = UserObject.add_user('denise')
+        user.commit()
         self.listener.user_added.reset_mock()
         with self.assertRaises(ValueError):
             UserObject.add_user('denise')
         # Check if listener called
         self.listener.user_added.assert_not_called()
 
     def test_add_user_with_password(self):
         """Add user to database with password."""
         userobj = UserObject.add_user('jo', 'password')
+        userobj.commit()
         self.assertIsNotNone(UserObject.get_user('jo'))
         # Check if listener called
         self.listener.user_added.assert_called_once_with(userobj)
 
     def test_delete_admin_user(self):
@@ -118,11 +119,12 @@ class UserObjectTest(rdiffweb.test.WebCase):
 
     def test_users(self):
         # Check admin exists
         self.assertEqual(1, UserObject.query.count())
         # Create user.
-        UserObject.add_user('annik')
+        user = UserObject.add_user('annik')
+        user.commit()
         users = UserObject.query.all()
         self.assertEqual(2, len(users))
         self.assertEqual('annik', users[1].username)
         # Then 2 user exists
         self.assertEqual(2, UserObject.query.count())
@@ -132,13 +134,15 @@ class UserObjectTest(rdiffweb.test.WebCase):
         user = UserObject.add_user('bernie', 'my-password')
         user.user_root = self.testcases
         user.role = UserObject.ADMIN_ROLE
         user.email = 'bernie@gmail.com'
         user.refresh_repos()
+        user.commit()
         self.assertEqual(['broker-repo', 'testcases'], sorted([r.name for r in user.repo_objs]))
         user.repo_objs[0].maxage = -1
         user.repo_objs[1].maxage = 3
+        user.commit()
 
         # Get user record.
         obj = UserObject.get_user('bernie')
         self.assertIsNotNone(obj)
         self.assertEqual('bernie', obj.username)
@@ -157,31 +161,33 @@ class UserObjectTest(rdiffweb.test.WebCase):
     def test_get_user_with_invalid_user(self):
         self.assertIsNone(UserObject.get_user('invalid'))
 
     def test_get_set(self):
         user = UserObject.add_user('larry', 'password')
+        user.add().commit()
 
         self.assertEqual('', user.email)
         self.assertEqual([], user.repo_objs)
         self.assertEqual('', user.user_root)
         self.assertEqual(False, user.is_admin)
         self.assertEqual(UserObject.USER_ROLE, user.role)
 
         user.user_root = self.testcases
         user.refresh_repos()
+        user.commit()
         self.listener.user_attr_changed.assert_called_with(user, {'user_root': ('', self.testcases)})
         self.listener.user_attr_changed.reset_mock()
         user = UserObject.get_user('larry')
         user.role = UserObject.ADMIN_ROLE
-        user.add()
+        user.commit()
         self.listener.user_attr_changed.assert_called_with(
             user, {'role': (UserObject.USER_ROLE, UserObject.ADMIN_ROLE)}
         )
         self.listener.user_attr_changed.reset_mock()
         user = UserObject.get_user('larry')
         user.email = 'larry@gmail.com'
-        user.add()
+        user.commit()
         self.listener.user_attr_changed.assert_called_with(user, {'email': ('', 'larry@gmail.com')})
         self.listener.user_attr_changed.reset_mock()
 
         self.assertEqual('larry@gmail.com', user.email)
         self.assertEqual(['broker-repo', 'testcases'], sorted([r.name for r in user.repo_objs]))
@@ -190,15 +196,16 @@ class UserObjectTest(rdiffweb.test.WebCase):
         self.assertEqual(UserObject.ADMIN_ROLE, user.role)
 
     def test_set_role_null(self):
         # Given a user
         user = UserObject.add_user('annik', 'password')
+        user.add().commit()
         # When trying to set the role to null
         user.role = None
         # Then an exception is raised
         with self.assertRaises(Exception):
-            user.add()
+            user.add().commit()
 
     @parameterized.expand(
         [
             (-1, True),
             (0, True),
@@ -210,11 +217,11 @@ class UserObjectTest(rdiffweb.test.WebCase):
     def test_is_admin(self, role, expected_is_admin):
         # Given a user
         user = UserObject.add_user('annik', 'password')
         # When setting the role value
         user.role = role
-        user.add()
+        user.commit()
         # Then the is_admin value get updated too
         self.assertEqual(expected_is_admin, user.is_admin)
 
     @parameterized.expand(
         [
@@ -228,39 +235,44 @@ class UserObjectTest(rdiffweb.test.WebCase):
     def test_is_maintainer(self, role, expected_is_maintainer):
         # Given a user
         user = UserObject.add_user('annik', 'password')
         # When setting the role value
         user.role = role
-        user.add()
+        user.commit()
         # Then the is_admin value get updated too
         self.assertEqual(expected_is_maintainer, user.is_maintainer)
 
     def test_set_password_update(self):
         # Given a user in database with a password
         userobj = UserObject.add_user('annik', 'password')
+        userobj.commit()
         self.listener.user_password_changed.reset_mock()
         # When updating the user's password
         userobj.set_password('new_password')
+        userobj.commit()
         # Then password is SSHA
         self.assertTrue(check_password('new_password', userobj.hash_password))
         # Check if listener called
         self.listener.user_password_changed.assert_called_once_with(userobj)
 
     def test_delete_user(self):
         # Given an existing user in database
         userobj = UserObject.add_user('vicky')
+        userobj.commit()
         self.assertIsNotNone(UserObject.get_user('vicky'))
         # When deleting that user
         userobj.delete()
+        userobj.commit()
         # Then user it no longer in database
         self.assertIsNone(UserObject.get_user('vicky'))
         # Then listner was called
         self.listener.user_deleted.assert_called_once_with('vicky')
 
     def test_set_password_empty(self):
         """Expect error when trying to update password of invalid user."""
         userobj = UserObject.add_user('john')
+        userobj.commit()
         with self.assertRaises(ValueError):
             self.assertFalse(userobj.set_password(''))
 
     def test_disk_quota(self):
         """
@@ -284,10 +296,11 @@ class UserObjectTest(rdiffweb.test.WebCase):
         # Read the pub key
         key = self._read_ssh_key()
         # Add the key to the user
         userobj = UserObject.get_user(self.USERNAME)
         userobj.add_authorizedkey(key)
+        userobj.commit()
 
         # validate
         keys = list(userobj.authorizedkeys)
         self.assertEqual(1, len(keys), "expecting one key")
         self.assertEqual("3c:99:ed:a7:82:a8:71:09:2c:15:3d:78:4a:8c:11:99", keys[0].fingerprint)
@@ -296,13 +309,15 @@ class UserObjectTest(rdiffweb.test.WebCase):
         # Read the pub key
         key = self._read_ssh_key()
         # Add the key to the user
         userobj = UserObject.get_user(self.USERNAME)
         userobj.add_authorizedkey(key)
+        userobj.commit()
         # Add the same key
         with self.assertRaises(DuplicateSSHKeyError):
             userobj.add_authorizedkey(key)
+            userobj.commit()
 
     def test_add_authorizedkey_with_file(self):
         """
         Add an ssh key for a user with an authorizedkey file.
         """
@@ -314,10 +329,11 @@ class UserObjectTest(rdiffweb.test.WebCase):
         open(filename, 'a').close()
 
         # Read the pub key
         key = self._read_ssh_key()
         userobj.add_authorizedkey(key)
+        userobj.commit()
 
         # Validate
         with open(filename, 'r') as fh:
             self.assertEqual(key, fh.read())
 
@@ -339,10 +355,11 @@ class UserObjectTest(rdiffweb.test.WebCase):
         keys = list(userobj.authorizedkeys)
         self.assertEqual(2, len(keys))
 
         # Remove a key
         userobj.delete_authorizedkey("9a:f1:69:3c:bc:5a:cd:02:5e:33:bc:cd:c0:01:eb:4c")
+        userobj.commit()
 
         # Validate
         keys = list(userobj.authorizedkeys)
         self.assertEqual(1, len(keys))
 
@@ -374,138 +391,50 @@ class UserObjectTest(rdiffweb.test.WebCase):
         userobj = UserObject.get_user(self.USERNAME)
         repos = sorted(userobj.repo_objs, key=lambda r: r.name)
         self.assertEqual(['broker-repo', 'testcases'], [r.name for r in repos])
         # When deleting a repository empty list
         repos[1].delete()
+        repos[1].commit()
         # Then the repository is removed from the list.
         self.assertEqual(['broker-repo'], sorted([r.name for r in userobj.repo_objs]))
 
     def test_refresh_repos_without_delete(self):
         # Given a user with invalid repositories
         userobj = UserObject.get_user(self.USERNAME)
         RepoObject.query.delete()
-        RepoObject(userid=userobj.userid, repopath='invalid').add()
+        RepoObject(userid=userobj.userid, repopath='invalid').add().commit()
         self.assertEqual(['invalid'], sorted([r.name for r in userobj.repo_objs]))
         # When updating the repository list without deletion
         userobj.refresh_repos()
+        userobj.commit()
         # Then the list invlaid the invalid repo and new repos
         self.assertEqual(['broker-repo', 'invalid', 'testcases'], sorted([r.name for r in userobj.repo_objs]))
 
     def test_refresh_repos_with_delete(self):
         # Given a user with invalid repositories
         userobj = UserObject.get_user(self.USERNAME)
         RepoObject.query.delete()
-        RepoObject(userid=userobj.userid, repopath='invalid').add()
+        RepoObject(userid=userobj.userid, repopath='invalid').add().commit()
         self.assertEqual(['invalid'], sorted([r.name for r in userobj.repo_objs]))
         # When updating the repository list without deletion
         userobj.refresh_repos(delete=True)
+        userobj.commit()
         # Then the list invlaid the invalid repo and new repos
         userobj.expire()
         self.assertEqual(['broker-repo', 'testcases'], sorted([r.name for r in userobj.repo_objs]))
 
     def test_refresh_repos_with_single_repo(self):
         # Given a user with invalid repositories
         userobj = UserObject.get_user(self.USERNAME)
         userobj.user_root = os.path.join(self.testcases, 'testcases')
         # When updating the repository list without deletion
         userobj.refresh_repos(delete=True)
+        userobj.commit()
         # Then the list invlaid the invalid repo and new repos
         userobj.expire()
         self.assertEqual([''], sorted([r.name for r in userobj.repo_objs]))
 
-    def test_add_access_token(self):
-        # Given a user with an email
-        userobj = UserObject.get_user(self.USERNAME)
-        userobj.email = 'test@examples.com'
-        userobj.add()
-        # When adding a new token
-        token = userobj.add_access_token('test')
-        # Then a new token get created
-        self.assertTrue(token)
-        tokenobj = Token.query.filter(Token.userid == userobj.userid).first()
-        self.assertTrue(tokenobj)
-        self.assertEqual(None, tokenobj.expiration_time)
-        self.assertEqual(None, tokenobj.access_time)
-        # Then an email is sent to the user.
-        self.listener.access_token_added.assert_called_once_with(userobj, 'test')
-        self.listener.queue_mail.assert_called_once()
-
-    def test_add_access_token_duplicate_name(self):
-        # Given a user with an existing token
-        userobj = UserObject.get_user(self.USERNAME)
-        userobj.add_access_token('test')
-        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
-        # When adding a new token with the same name
-        with self.assertRaises(ValueError):
-            userobj.add_access_token('test')
-        # Then token is not created
-        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
-        # Then an email is not sent.
-        self.listener.access_token_added.assert_called_once_with(userobj, 'test')
-
-    def test_delete_access_token(self):
-        # Given a user with an existing token
-        userobj = UserObject.get_user(self.USERNAME)
-        userobj.add_access_token('test')
-        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
-        # When deleting an access token
-        userobj.delete_access_token('test')
-        # Then Token get deleted
-        self.assertEqual(0, Token.query.filter(Token.userid == userobj.userid).count())
-
-    def test_delete_access_token_invalid(self):
-        # Given a user with an existing token
-        userobj = UserObject.get_user(self.USERNAME)
-        userobj.add_access_token('test')
-        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
-        # When deleting an invalid access token
-        with self.assertRaises(ValueError):
-            userobj.delete_access_token('invalid')
-        # Then Token not deleted
-        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
-
-    def test_delete_user_remove_access_tokens(self):
-        # Given a user with an existing token
-        userobj = UserObject.add_user('testuser', 'password')
-        userobj.add_access_token('test')
-        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
-        # When deleting the user
-        userobj.delete()
-        # Then Token get deleted
-        self.assertEqual(0, Token.query.filter(Token.userid == userobj.userid).count())
-
-    def test_verify_access_token(self):
-        # Given a user with an existing token
-        userobj = UserObject.get_user(self.USERNAME)
-        token = userobj.add_access_token('test')
-        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
-        # When validating the token
-        # Then token is valid
-        self.assertTrue(userobj.validate_access_token(token))
-
-    def test_verify_access_token_with_expired(self):
-        # Given a user with an existing token
-        userobj = UserObject.get_user(self.USERNAME)
-        token = userobj.add_access_token(
-            'test', expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1)
-        )
-        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
-        # When validating the token
-        # Then token is invalid
-        self.assertFalse(userobj.validate_access_token(token))
-        # Then token get removed
-        self.assertEqual(0, Token.query.filter(Token.userid == userobj.userid).count())
-
-    def test_verify_access_token_with_invalid(self):
-        # Given a user with an existing token
-        userobj = UserObject.get_user(self.USERNAME)
-        userobj.add_access_token('test', expiration_time=datetime.datetime.now())
-        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
-        # When validating the token
-        # Then token is invalid
-        self.assertFalse(userobj.validate_access_token('invalid'))
-
 
 class UserObjectWithAdminPassword(rdiffweb.test.WebCase):
 
     # password: test
     default_config = {'admin-password': '{SSHA}wbSK4hlEX7mtGJplFi2oN6ABm6Y3Bo1e'}
@@ -521,11 +450,10 @@ class UserObjectWithAdminPassword(rdiffweb.test.WebCase):
         userobj = UserObject.get_user(self.USERNAME)
         self.assertIsNotNone(userobj)
         self.assertEqual('{SSHA}wbSK4hlEX7mtGJplFi2oN6ABm6Y3Bo1e', userobj.hash_password)
         self.assertTrue(check_password('test', userobj.hash_password))
 
-    def test_set_password(self):
         # Given admin-password is configure
         # When trying to update admin password
         # Then an exception is raised
         userobj = UserObject.get_user(self.USERNAME)
         with self.assertRaises(ValueError):
diff --git a/rdiffweb/core/tests/test_login.py b/rdiffweb/core/tests/test_login.py
index 3604b7c96..65a0762d2 100644
--- a/rdiffweb/core/tests/test_login.py
+++ b/rdiffweb/core/tests/test_login.py
@@ -43,19 +43,21 @@ class LoginAbstractTest(rdiffweb.test.WebCase):
 
 class LoginTest(LoginAbstractTest):
     def test_login(self):
         # Given a valid user in database with a password
         userobj = UserObject.add_user('tom', 'password')
+        userobj.commit()
         # When trying to login with valid password
         login = cherrypy.engine.publish('login', 'tom', 'password')
         # Then login is successful
         self.assertEqual(login, [userobj])
         # Check if listener called
         self.listener.user_login.assert_called_once_with(userobj)
 
     def test_login_with_invalid_password(self):
-        UserObject.add_user('jeff', 'password')
+        userobj = UserObject.add_user('jeff', 'password')
+        userobj.commit()
         self.assertFalse(any(cherrypy.engine.publish('login', 'jeff', 'invalid')))
         # password is case sensitive
         self.assertFalse(any(cherrypy.engine.publish('login', 'jeff', 'Password')))
         # Match entire password
         self.assertFalse(any(cherrypy.engine.publish('login', 'jeff', 'pass')))
diff --git a/rdiffweb/core/tests/test_notification.py b/rdiffweb/core/tests/test_notification.py
index 6fe44b493..3d82a503c 100644
--- a/rdiffweb/core/tests/test_notification.py
+++ b/rdiffweb/core/tests/test_notification.py
@@ -51,14 +51,14 @@ class NotificationJobTest(rdiffweb.test.WebCase):
         """
         # Given a user with an email address and a repository with a maxage
         # Set user config
         user = UserObject.get_user(self.USERNAME)
         user.email = 'test@test.com'
-        user.add()
+        user.commit()
         repo = RepoObject.query.filter(RepoObject.user == user, RepoObject.repopath == self.REPO).first()
         repo.maxage = 1
-        repo.add()
+        repo.commit()
         # When running notification_job
         cherrypy.notification.notification_job()
 
         # Then an email is queue for this user
         self.listener.queue_email.assert_called_once_with(
@@ -69,15 +69,15 @@ class NotificationJobTest(rdiffweb.test.WebCase):
 
     def test_notification_job_undefined_last_backup_date(self):
         # Given a valid user with a repository configured for notification
         user = UserObject.get_user(self.USERNAME)
         user.email = 'test@test.com'
-        user.add()
+        user.add().commit()
         # Given a repo with last_backup_date None
         repo = RepoObject.query.filter(RepoObject.user == user, RepoObject.repopath == 'broker-repo').first()
         repo.maxage = 1
-        repo.add()
+        repo.add().commit()
         self.assertIsNone(repo.last_backup_date)
 
         # When Notification job is running
         cherrypy.notification.notification_job()
 
@@ -90,14 +90,14 @@ class NotificationJobTest(rdiffweb.test.WebCase):
 
     def test_notification_job_without_notification(self):
         # Given a valid user with a repository configured without notification (-1)
         user = UserObject.get_user(self.USERNAME)
         user.email = 'test@test.com'
-        user.add()
+        user.add().commit()
         repo = RepoObject.query.filter(RepoObject.user == user, RepoObject.repopath == self.REPO).first()
         repo.maxage = -1
-        repo.add()
+        repo.add().commit()
 
         # Call notification.
         cherrypy.notification.notification_job()
 
         # Expect it to be called.
@@ -121,17 +121,17 @@ class NotificationPluginTest(rdiffweb.test.WebCase):
 
     def test_email_changed(self):
         # Given a user with an email address
         user = UserObject.get_user(self.USERNAME)
         user.email = 'original_email@test.com'
-        user.add()
+        user.add().commit()
         self.listener.queue_email.reset_mock()
 
         # When updating the user's email
         user = UserObject.get_user(self.USERNAME)
         user.email = 'email_changed@test.com'
-        user.add()
+        user.add().commit()
 
         # Then a email is queue to notify the user.
         self.listener.queue_email.assert_called_once_with(
             to='original_email@test.com',
             subject='Email address changed',
@@ -140,26 +140,30 @@ class NotificationPluginTest(rdiffweb.test.WebCase):
 
     def test_email_updated_with_same_value(self):
         # Given a user with an email
         user = UserObject.get_user(self.USERNAME)
         user.email = 'email_changed@test.com'
+        user.add().commit()
         self.listener.queue_email.reset_mock()
 
         # When updating the user's email with the same value
         user.email = 'email_changed@test.com'
+        user.add().commit()
 
         # Then no email are sent to the user
         self.listener.queue_email.assert_not_called()
 
     def test_password_change_notification(self):
         # Given a user with a email.
         user = UserObject.get_user(self.USERNAME)
         user.email = 'password_change@test.com'
+        user.add().commit()
         self.listener.queue_email.reset_mock()
 
         # When updating the user password
         user.set_password('new_password')
+        user.add().commit()
 
         # Then a email is send to the user
         self.listener.queue_email.assert_called_once_with(
             to='password_change@test.com',
             subject='Password changed',
@@ -169,14 +173,16 @@ class NotificationPluginTest(rdiffweb.test.WebCase):
     def test_password_change_with_same_value(self):
         # Given a user with a email.
         user = UserObject.get_user(self.USERNAME)
         user.email = 'password_change@test.com'
         user.set_password('new_password')
+        user.add().commit()
         self.listener.queue_email.reset_mock()
 
         # When updating the user password with the same value
         user.set_password('new_password')
+        user.add().commit()
 
         # Then an email is sent to the user
         self.listener.queue_email.assert_called_once_with(
             to='password_change@test.com',
             subject='Password changed',
diff --git a/rdiffweb/core/tests/test_quota.py b/rdiffweb/core/tests/test_quota.py
index b220af769..edd4556e6 100644
--- a/rdiffweb/core/tests/test_quota.py
+++ b/rdiffweb/core/tests/test_quota.py
@@ -38,26 +38,29 @@ class QuotaPluginTest(test.WebCase):
     }
 
     def test_get_disk_usage(self):
         # Given a user
         userobj = UserObject.add_user('bob')
+        userobj.commit()
         # When querying quota for a userobj
         result = cherrypy.engine.publish('get_disk_usage', userobj)
         # Then quota return a value
         self.assertEqual(21474836, result[0])
 
     def test_get_disk_quota(self):
         # Given a user
         userobj = UserObject.add_user('bob')
+        userobj.commit()
         # When querying quota for a userobj
         result = cherrypy.engine.publish('get_disk_quota', userobj)
         # Then quota return a value
         self.assertEqual(123456, result[0])
 
     def test_set_disk_quota(self):
         # Given a used cmd
         userobj = UserObject.add_user('bob')
+        userobj.commit()
         # When querying quota for a userobj
         results = cherrypy.engine.publish('set_disk_quota', userobj, 98765)
         # Then quota return a value
         self.assertEqual([98765], results)
 
@@ -71,10 +74,11 @@ class QuotaPluginTestWithFailure(test.WebCase):
     }
 
     def test_set_disk_quota_with_failure(self):
         # Given a user object
         userobj = UserObject.add_user('bob')
+        userobj.commit()
         # When settings the quota
         results = cherrypy.engine.publish('set_disk_quota', userobj, 98765)
         # Then False is returned
         self.assertEqual([False], results)
 
@@ -106,19 +110,21 @@ class QuotaPluginTestWithUndefinedCmd(test.WebCase):
 
     def test_get_disk_usage_with_empty_user_root(self):
         # Given a user with an empty user_root.
         userobj = UserObject.add_user('bob')
         userobj.user_root = ''
+        userobj.commit()
         # When getting disk usage
         results = cherrypy.engine.publish('get_disk_usage', userobj)
         # Then default disk usage is return
         self.assertEqual([0], results)
 
     def test_get_disk_usage_with_invalid_user_root(self):
         # Given a user with an invalid user_root.
         userobj = UserObject.add_user('bob')
         userobj.user_root = 'invalid'
+        userobj.commit()
         # When getting disk usage
         results = cherrypy.engine.publish('get_disk_usage', userobj)
         # Then default disk usage is return
         self.assertEqual([0], results)
 
diff --git a/rdiffweb/core/tests/test_rdw_templating.py b/rdiffweb/core/tests/test_rdw_templating.py
index c677022ff..c8d8bab3e 100644
--- a/rdiffweb/core/tests/test_rdw_templating.py
+++ b/rdiffweb/core/tests/test_rdw_templating.py
@@ -20,11 +20,11 @@ import unittest
 import cherrypy
 
 from rdiffweb.core.librdiff import RdiffTime
 from rdiffweb.core.model import RepoObject, UserObject
 from rdiffweb.core.rdw_templating import _ParentEntry, attrib, do_format_lastupdated, list_parents, url_for
-from rdiffweb.test import AppTestCase, WebCase
+from rdiffweb.test import WebCase
 
 
 class TemplateManagerTest(unittest.TestCase):
     def test_attrib(self):
         # Single value
@@ -89,11 +89,11 @@ class TemplateManagerTest(unittest.TestCase):
         self.assertEqual('4 weeks ago', do_format_lastupdated(RdiffTime(value=1589127124), now=1591978846))
         self.assertEqual('5 months ago', do_format_lastupdated(RdiffTime(value=1578672724), now=1591978846))
         self.assertEqual('4 years ago', do_format_lastupdated(RdiffTime(value=1452442324), now=1591978846))
 
 
-class ListParentsTest(AppTestCase):
+class ListParentsTest(WebCase):
     def test_list_parents_with_root_dir(self):
         repo, path = RepoObject.get_repo_path(b'admin/testcases', as_user=UserObject.get_user('admin'))
         self.assertEqual(list_parents(repo, path), [_ParentEntry(path=b'', display_name='testcases')])
 
     def test_list_parents_with_root_subdir(self):
diff --git a/rdiffweb/rdw_app.py b/rdiffweb/rdw_app.py
index 34a1e677a..6bc5c77d1 100644
--- a/rdiffweb/rdw_app.py
+++ b/rdiffweb/rdw_app.py
@@ -230,11 +230,12 @@ class RdiffwebApp(Application):
         # Define TEMP env
         if self._tempdir:
             os.environ["TMPDIR"] = self._tempdir
 
         # create user manager
-        UserObject.create_admin_user(cfg.admin_user, cfg.admin_password)
+        user = UserObject.create_admin_user(cfg.admin_user, cfg.admin_password)
+        user.commit()
 
     @property
     def currentuser(self):
         """
         Proxy property to get current user from cherrypy request object.
diff --git a/rdiffweb/templates/access_token_added.html b/rdiffweb/templates/access_token_added.html
index 5142d38f7..53b6bf5c8 100644
--- a/rdiffweb/templates/access_token_added.html
+++ b/rdiffweb/templates/access_token_added.html
@@ -1,10 +1,10 @@
 <html>
   <head></head>
   <body>
     {% trans username=user.username %}Hey {{ username }},{% endtrans %}
-    <p>{% trans %}A new access token, named "{{ test }}", has been created.{% endtrans %}</p>
+    <p>{% trans %}A new access token, named "{{ name }}", has been created.{% endtrans %}</p>
     <p>
       {% trans %}If you did not make this change and believe your account has been compromised, please contact your administrator.{% endtrans %}
     </p>
   </body>
 </html>
diff --git a/rdiffweb/test.py b/rdiffweb/test.py
index 2cd452c26..821b6cb83 100644
--- a/rdiffweb/test.py
+++ b/rdiffweb/test.py
@@ -49,59 +49,10 @@ def create_testcases_repo(app):
     new = str(tempfile.mkdtemp(prefix='rdiffweb_tests_'))
     subprocess.check_call(['tar', '-zxf', testcases], cwd=new)
     return new
 
 
-class AppTestCase(unittest.TestCase):
-
-    REPO = 'testcases'
-
-    USERNAME = 'admin'
-
-    PASSWORD = 'admin123'
-
-    default_config = {}
-
-    app_class = RdiffwebApp
-
-    @classmethod
-    def setup_class(cls):
-        if cls is AppTestCase:
-            raise unittest.SkipTest("%s is an abstract base class" % cls.__name__)
-
-    @classmethod
-    def teardown_class(cls):
-        pass
-
-    def setUp(self):
-        # Allow defining a custom database uri for testing.
-        self.database_dir = tempfile.mkdtemp(prefix='rdiffweb_tests_db_')
-        uri = os.path.join(self.database_dir, 'rdiffweb.tmp.db')
-        uri = os.environ.get('RDIFFWEB_TEST_DATABASE_URI', uri)
-        self.default_config['database-uri'] = uri
-        cfg = self.app_class.parse_args(
-            args=[], config_file_contents='\n'.join('%s=%s' % (k, v) for k, v in self.default_config.items())
-        )
-        # Create Application
-        self.app = self.app_class(cfg)
-        # Create repositories
-        self.testcases = create_testcases_repo(self.app)
-        # Register repository
-        admin_user = UserObject.get_user(self.USERNAME)
-        if admin_user:
-            admin_user.user_root = self.testcases
-            admin_user.refresh_repos()
-
-    def tearDown(self):
-        if hasattr(self, 'database_dir'):
-            shutil.rmtree(self.database_dir)
-            delattr(self, 'database_dir')
-        if hasattr(self, 'testcases'):
-            shutil.rmtree(self.testcases)
-            delattr(self, 'testcases')
-
-
 class WebCase(helper.CPWebCase):
     """
     Helper class for the rdiffweb test suite.
     """
 
@@ -150,25 +101,27 @@ class WebCase(helper.CPWebCase):
     def setUp(self):
         helper.CPWebCase.setUp(self)
         cherrypy.tools.db.drop_all()
         cherrypy.tools.db.create_all()
         # Create default admin
-        UserObject.create_admin_user(self.USERNAME, self.PASSWORD)
+        admin_user = UserObject.create_admin_user(self.USERNAME, self.PASSWORD)
+        admin_user.commit()
         # Create testcases repo
         self.testcases = create_testcases_repo(self.app)
-        admin_user = UserObject.get_user(self.USERNAME)
         if admin_user:
             admin_user.user_root = self.testcases
             admin_user.refresh_repos()
+            admin_user.commit()
         # Login to web application.
         if self.login:
             self._login()
 
     def tearDown(self):
         if hasattr(self, 'testcases'):
             shutil.rmtree(self.testcases)
             delattr(self, 'testcases')
+        cherrypy.tools.db.drop_all()
 
     @property
     def app(self):
         """
         Return reference to Rdiffweb application.
diff --git a/rdiffweb/tools/db.py b/rdiffweb/tools/db.py
index 0aee0d07b..76350b90a 100644
--- a/rdiffweb/tools/db.py
+++ b/rdiffweb/tools/db.py
@@ -18,12 +18,13 @@
 SQLAlchemy Tool for CherryPy.
 '''
 import logging
 
 import cherrypy
-from sqlalchemy import create_engine, event
+from sqlalchemy import create_engine, event, inspect
 from sqlalchemy.engine import Engine
+from sqlalchemy.exc import SQLAlchemyError
 from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
 from sqlalchemy.orm import scoped_session, sessionmaker
 
 logger = logging.getLogger(__name__)
 
@@ -37,10 +38,35 @@ def _set_sqlite_journal_mode_wal(connection, connection_record):
         cursor = connection.cursor()
         cursor.execute('PRAGMA journal_mode=WAL;')
         cursor.close()
 
 
+def _get_model_changes(model):
+    """
+    Return a dictionary containing changes made to the model since it was
+    fetched from the database.
+
+    The dictionary is of the form {'property_name': [old_value, new_value]}
+    """
+    state = inspect(model)
+    changes = {}
+    for attr in state.attrs:
+        hist = attr.history
+        if not hist.has_changes():
+            continue
+        if isinstance(attr.value, (list, tuple)) or len(hist.deleted) > 1 or len(hist.added) > 1:
+            # If array, store array
+            changes[attr.key] = [hist.deleted, hist.added]
+        else:
+            # If primitive, store primitive
+            changes[attr.key] = [
+                hist.deleted[0] if len(hist.deleted) >= 1 else None,
+                hist.added[0] if len(hist.added) >= 1 else None,
+            ]
+    return changes
+
+
 class Base:
     '''
     Extends declarative base to provide convenience methods to models similar to
     functionality found in Elixir. Works in python3.
 
@@ -51,39 +77,36 @@ class Base:
     User.query # returns session.query(User)
     User.query.all() # instead of session.query(User).all()
     changed = User.from_dict({}) # update record based on dict argument passed in and returns any keys changed
     '''
 
-    def add(self, commit=True):
+    def add(self):
         """
         Add current object to session.
         """
         self.__class__.session.add(self)
-        if commit:
-            self.__class__.session.commit()
         return self
 
-    def delete(self, commit=True):
-        """
-        Delete current object to session.
-        """
+    def delete(self):
         self.__class__.session.delete(self)
-        if commit:
-            self.__class__.session.commit()
         return self
 
-    def merge(self, commit=True):
-        """
-        Merge current object to session.
-        """
-        self.__class__.session.merge(self)
-        if commit:
-            self.__class__.session.commit()
+    def commit(self):
+        self.__class__.session.commit()
+        return self
+
+    def flush(self):
+        self.__class__.session.flush()
         return self
 
     def expire(self):
         self.__class__.session.expire(self)
+        return self
+
+    def rollback(self):
+        self.__class__.session.rollback()
+        return self
 
 
 class BaseExtensions(DeclarativeMeta):
     @property
     def query(self):
@@ -115,17 +138,19 @@ class SQLA(cherrypy.Tool):
             debug = cherrypy.config.get('tools.db.debug')
             base.metadata.bind = create_engine(dburi)
             if debug:
                 logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
         base.metadata.create_all()
+        self.get_session().commit()
 
     def drop_all(self):
         # Release opened sessions.
         self.on_end_resource()
         # Drop all
         base = self.get_base()
         base.metadata.drop_all()
+        self.get_session().commit()
 
     def get_base(self):
         if self._base is None:
             self._base = declarative_base(metaclass=BaseExtensions, cls=Base)
         return self._base
@@ -138,16 +163,20 @@ class SQLA(cherrypy.Tool):
 
     def on_end_resource(self):
         if self._session is None:
             return
         try:
-            self._session.flush()
-            self._session.commit()
-        except Exception:
-            logger.exception('error trying to flush and commit session')
+            # When terminating, raise an error if objects are not commit.
+            if self._session.dirty or self._session.new or self._session.deleted:
+                changes = ', '.join([str(_get_model_changes(obj)) for obj in self._session.dirty])
+                logger.exception(
+                    'session is dirty, some database object(s) are not commited, this indicate a bug in the application '
+                    'dirty %s new %s deleted %s' % (changes, self._session.new, self._session.deleted)
+                )
+                raise SQLAlchemyError('session is dirty')
+        finally:
             self._session.rollback()
             self._session.expunge_all()
-        finally:
             self._session.remove()
 
 
 cherrypy.tools.db = SQLA()

Vulnerable code:         if userobj.validate_access_token(password):
        u.refresh_repos()
                cherrypy.engine.publish('schedule_task', repo.delete, path)
        self.app.currentuser.refresh_repos()
        user.add()
            user.refresh_repos(delete=True)
        userobj.add()
        data.add()
        UserObject.add_user('test', 'test123')
        UserObject.add_user('newuser')
        UserObject.add_user('test1')
                user.delete()
        user.add()
        user.add()
        RepoObject(userid=userobj.userid, repopath='testcases/broker-repo').add()
        RepoObject(userid=userobj.userid, repopath='testcases/testcases').add()
        user.add()
        user_obj.add()
        admin.add()
        admin.add()
        admin.add()
        admin.add()
        admin.add()
        admin.add()
        UserObject.add_user('otheruser', password='password')
        admin.add()
        userobj.add()
        userobj.add()
        userobj.add()
        userobj.add()
from rdiffweb.core.model import RepoObject, UserObject
        self.listener.user_password_changed.reset_mock()
        RepoObject(userid=userobj.userid, repopath='invalid').add()
        userobj.add()
        userobj.add()
        userobj.add()
        admin.add()
        admin.add()
        admin.add()
        admin.add()
        admin.add()
        userobj.add()
                ).add()
            userobj.add()
            row.add()
            row.add()
            as_user.refresh_repos()
        super().delete()
        session.add()
import datetime
                SshKey(userid=self.userid, fingerprint=key.fingerprint, key=key.getvalue()).add()
                SshKey.session.rollback()
            obj = Token(userid=self.userid, name=name, hash_token=hash_password(token), expiration_time=expiration_time)
            obj.add()
            Token.session.rollback()
        Base.delete(self)
        logger.info("updating user password [%s]", self.username)
            # If token expired. Let delete it.
                access_token.delete()
                # When it matches, let update the record.
                access_token.access_time = datetime.datetime.utcnow
                return True
        RepoObject(userid=userobj.userid, repopath='/testcases').add()
        RepoObject(userid=userobj.userid, repopath='/testcases/home/admin/data').add()
        RepoObject(userid=userobj.userid, repopath='/testcases').add()

        repo_obj.add()
        repo_obj.add()
        repo_obj.add()
        repo_obj = RepoObject(user=userobj, repopath='repopath').add()
        repo_obj = RepoObject(user=userobj, repopath='repopath').add()
        repo_obj = RepoObject(user=userobj, repopath='repopath').add()
import datetime
from rdiffweb.core.model import DuplicateSSHKeyError, RepoObject, Token, UserObject
        self.assertIsNotNone(userobj)
        self.assertIsNotNone(userobj)
        UserObject.add_user('denise')
        UserObject.add_user('annik')
        user.add()
        user.add()
            user.add()
        user.add()
        user.add()
        RepoObject(userid=userobj.userid, repopath='invalid').add()
        RepoObject(userid=userobj.userid, repopath='invalid').add()
    def test_add_access_token(self):
        # Given a user with an email
        userobj = UserObject.get_user(self.USERNAME)
        userobj.email = 'test@examples.com'
        userobj.add()
        # When adding a new token
        token = userobj.add_access_token('test')
        # Then a new token get created
        self.assertTrue(token)
        tokenobj = Token.query.filter(Token.userid == userobj.userid).first()
        self.assertTrue(tokenobj)
        self.assertEqual(None, tokenobj.expiration_time)
        self.assertEqual(None, tokenobj.access_time)
        # Then an email is sent to the user.
        self.listener.access_token_added.assert_called_once_with(userobj, 'test')
        self.listener.queue_mail.assert_called_once()

    def test_add_access_token_duplicate_name(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        userobj.add_access_token('test')
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When adding a new token with the same name
        with self.assertRaises(ValueError):
            userobj.add_access_token('test')
        # Then token is not created
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # Then an email is not sent.
        self.listener.access_token_added.assert_called_once_with(userobj, 'test')

    def test_delete_access_token(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        userobj.add_access_token('test')
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When deleting an access token
        userobj.delete_access_token('test')
        # Then Token get deleted
        self.assertEqual(0, Token.query.filter(Token.userid == userobj.userid).count())

    def test_delete_access_token_invalid(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        userobj.add_access_token('test')
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When deleting an invalid access token
        with self.assertRaises(ValueError):
            userobj.delete_access_token('invalid')
        # Then Token not deleted
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())

    def test_delete_user_remove_access_tokens(self):
        # Given a user with an existing token
        userobj = UserObject.add_user('testuser', 'password')
        userobj.add_access_token('test')
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When deleting the user
        userobj.delete()
        # Then Token get deleted
        self.assertEqual(0, Token.query.filter(Token.userid == userobj.userid).count())

    def test_verify_access_token(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        token = userobj.add_access_token('test')
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When validating the token
        # Then token is valid
        self.assertTrue(userobj.validate_access_token(token))

    def test_verify_access_token_with_expired(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        token = userobj.add_access_token(
            'test', expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1)
        )
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When validating the token
        # Then token is invalid
        self.assertFalse(userobj.validate_access_token(token))
        # Then token get removed
        self.assertEqual(0, Token.query.filter(Token.userid == userobj.userid).count())

    def test_verify_access_token_with_invalid(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        userobj.add_access_token('test', expiration_time=datetime.datetime.now())
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When validating the token
        # Then token is invalid
        self.assertFalse(userobj.validate_access_token('invalid'))

    def test_set_password(self):
        UserObject.add_user('jeff', 'password')
        user.add()
        repo.add()
        user.add()
        repo.add()
        user.add()
        repo.add()
        user.add()
        user.add()
from rdiffweb.test import AppTestCase, WebCase
class ListParentsTest(AppTestCase):
        UserObject.create_admin_user(cfg.admin_user, cfg.admin_password)
    <p>{% trans %}A new access token, named "{{ test }}", has been created.{% endtrans %}</p>
class AppTestCase(unittest.TestCase):

    REPO = 'testcases'

    USERNAME = 'admin'

    PASSWORD = 'admin123'

    default_config = {}

    app_class = RdiffwebApp

    @classmethod
    def setup_class(cls):
        if cls is AppTestCase:
            raise unittest.SkipTest("%s is an abstract base class" % cls.__name__)

    @classmethod
    def teardown_class(cls):
        pass

    def setUp(self):
        # Allow defining a custom database uri for testing.
        self.database_dir = tempfile.mkdtemp(prefix='rdiffweb_tests_db_')
        uri = os.path.join(self.database_dir, 'rdiffweb.tmp.db')
        uri = os.environ.get('RDIFFWEB_TEST_DATABASE_URI', uri)
        self.default_config['database-uri'] = uri
        cfg = self.app_class.parse_args(
            args=[], config_file_contents='\n'.join('%s=%s' % (k, v) for k, v in self.default_config.items())
        )
        # Create Application
        self.app = self.app_class(cfg)
        # Create repositories
        self.testcases = create_testcases_repo(self.app)
        # Register repository
        admin_user = UserObject.get_user(self.USERNAME)
        if admin_user:
            admin_user.user_root = self.testcases
            admin_user.refresh_repos()

    def tearDown(self):
        if hasattr(self, 'database_dir'):
            shutil.rmtree(self.database_dir)
            delattr(self, 'database_dir')
        if hasattr(self, 'testcases'):
            shutil.rmtree(self.testcases)
            delattr(self, 'testcases')


        UserObject.create_admin_user(self.USERNAME, self.PASSWORD)
        admin_user = UserObject.get_user(self.USERNAME)
from sqlalchemy import create_engine, event
    def add(self, commit=True):
        if commit:
            self.__class__.session.commit()
    def delete(self, commit=True):
        """
        Delete current object to session.
        """
        if commit:
            self.__class__.session.commit()
    def merge(self, commit=True):
        """
        Merge current object to session.
        """
        self.__class__.session.merge(self)
        if commit:
            self.__class__.session.commit()
            self._session.flush()
            self._session.commit()
        except Exception:
            logger.exception('error trying to flush and commit session')
        finally:
Fixed code: * Revoke previous user's sessions on password change [CVE-2022-3362](https://nvd.nist.gov/vuln/detail/CVE-2022-3362)
        access_token = userobj.validate_access_token(password)
        if access_token:
            access_token.accessed()
            access_token.commit()
        if u.refresh_repos():
            u.commit()
                    session.commit()
        userobj.commit()
                    user.commit()
def delete_repo(repoobj, path):
    repoobj.delete(path)
    repoobj.commit()


                cherrypy.engine.publish('schedule_task', delete_repo, repo, path)
        if self.app.currentuser.refresh_repos():
            self.app.currentuser.commit()
        user.commit()
            user.commit()
            if user.refresh_repos(delete=True):
                user.commit()
            userobj.commit()
            userobj.commit()
        userobj.commit()
                    session.commit()
            userobj.commit()
            userobj.rollback()
            userobj.commit()
            userobj.commit()
        userobj.commit()
        userobj.commit()
        data.commit()
        userobj = UserObject.add_user('test', 'test123')
        userobj.commit()
        user = UserObject.add_user('newuser')
        user.commit()
        userobj = UserObject.add_user('test1')
        userobj.commit()
                user.delete().commit()
        user.commit()
        user.commit()
        RepoObject(userid=userobj.userid, repopath='testcases/broker-repo').add().commit()
        RepoObject(userid=userobj.userid, repopath='testcases/testcases').add().commit()
        user.commit()
        user_obj.commit()
        admin.commit()
        admin.commit()
        user_obj.commit()
        user_obj.commit()
        user_obj.commit()
        user_obj.commit()
        admin.commit()
        admin.commit()
        user_obj.commit()
        admin.commit()
        admin.commit()
        userobj = UserObject.add_user('otheruser', password='password')
        userobj.commit()
        admin.commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
from rdiffweb.core.model import RepoObject, SessionObject, UserObject
        # Given a user with 3 active sessions
        self.cookies = None
        self._login(self.USERNAME, self.PASSWORD)
        self.cookies = None
        self._login(self.USERNAME, self.PASSWORD)
        self.assertEqual(3, SessionObject.query.count())
        self.listener.user_password_changed.reset_mock()
        # Then all users session get deleted except our own session.
        self.assertEqual(1, SessionObject.query.count())
        self.assertEqual(self.session_id, SessionObject.query.first().id)
        RepoObject(userid=userobj.userid, repopath='invalid').add().commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
        user.commit()
        user.commit()
        user.commit()
        user.commit()
        user.commit()
        userobj.commit()
        user_obj.commit()
        admin.commit()
        user_obj.commit()
        admin.commit()
        admin.commit()
        user_obj.commit()
        admin.commit()
        user_obj.commit()
        admin.commit()
        userobj.commit()
                ).commit()
            userobj.commit()
            row.commit()
            row.commit()
            if as_user.refresh_repos():
                as_user.commit()
        return super().delete()
        session.add().commit()
        SessionObject.session.commit()
            SessionObject.session.commit()
from cherrypy.process.plugins import SimplePlugin

    def accessed(self):
        self.access_time = datetime.datetime.utcnow()


class TokenCleanup(SimplePlugin):

    execution_time = '23:00'

    def start(self):
        self.bus.log('Start Token Clean Up plugin')
        self.bus.publish('schedule_job', self.execution_time, self.clean_up)

    start.priority = 55

    def stop(self):
        self.bus.log('Stop Token Clean Up plugin')
        self.bus.publish('unschedule_job', self.clean_up)

    stop.priority = 45

    def clean_up(self):
        Token.query.filter(Token.expiration_time <= datetime.datetime.now()).delete()
        Token.session.commit()


cherrypy.token_cleanup = TokenCleanup(cherrypy.engine)
cherrypy.token_cleanup.subscribe()

cherrypy.config.namespaces['token_cleanup'] = lambda key, value: setattr(cherrypy.token_cleanup, key, value)
from ._session import SessionObject
        return userobj
                SshKey(userid=self.userid, fingerprint=key.fingerprint, key=key.getvalue()).add().flush()
            Token(
                userid=self.userid, name=name, hash_token=hash_password(token), expiration_time=expiration_time
            ).add().flush()
        return Base.delete(self)
        # Store password
        logger.info("updating user password [%s] and revoke sessions", self.username)
        # Revoke other session to force re-login
        session_id = cherrypy.serving.session.id if getattr(cherrypy.serving, 'session', None) else None
        SessionObject.query.filter(
            SessionObject.username == self.username,
            SessionObject.id != session_id,
        ).delete()

                # When it matches, return the record.
                return access_token
        RepoObject(userid=userobj.userid, repopath='/testcases').add().commit()
        RepoObject(userid=userobj.userid, repopath='/testcases/home/admin/data').add().commit()
        RepoObject(userid=userobj.userid, repopath='/testcases').add().commit()
        user.commit()
        user.commit()
        other.commit()
        user.commit()
        other.commit()
        repo_obj.commit()
        repo_obj.commit()
        repo_obj.commit()
        repo_obj = RepoObject(user=userobj, repopath='repopath').add().commit()
        repo_obj = RepoObject(user=userobj, repopath='repopath').add().commit()
        repo_obj = RepoObject(user=userobj, repopath='repopath').add().commit()
# -*- coding: utf-8 -*-
# rdiffweb, A web interface to rdiff-backup repositories
# Copyright (C) 2012-2021 rdiffweb contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
Created on June 30, 2022

Module to test `user` model.

@author: Patrik Dufresne <patrik@ikus-soft.com>
"""
import datetime
from io import open
from unittest.mock import MagicMock

import cherrypy
import pkg_resources

import rdiffweb.test
from rdiffweb.core.model import Token, UserObject


class TokenTest(rdiffweb.test.WebCase):
    def _read_ssh_key(self):
        """Readthe pub key from test packages"""
        filename = pkg_resources.resource_filename('rdiffweb.core.tests', 'test_publickey_ssh_rsa.pub')
        with open(filename, 'r', encoding='utf8') as f:
            return f.readline()

    def _read_authorized_keys(self):
        """Read the content of test_authorized_keys"""
        filename = pkg_resources.resource_filename('rdiffweb.core.tests', 'test_authorized_keys')
        with open(filename, 'r', encoding='utf8') as f:
            return f.read()

    def setUp(self):
        super().setUp()
        self.listener = MagicMock()
        cherrypy.engine.subscribe('access_token_added', self.listener.access_token_added, priority=50)
        cherrypy.engine.subscribe('queue_mail', self.listener.queue_mail, priority=50)

    def tearDown(self):
        cherrypy.engine.unsubscribe('access_token_added', self.listener.access_token_added)
        cherrypy.engine.unsubscribe('queue_mail', self.listener.queue_mail)
        return super().tearDown()

    def test_check_schedule(self):
        # Given the application is started
        # Then remove_older job should be schedule
        self.assertEqual(1, len([job for job in cherrypy.scheduler.list_jobs() if job.name == 'clean_up']))

    def test_clean_up_without_expired(self):
        # Given a user with 3 Token
        user = UserObject.get_user(self.USERNAME)
        user.add_access_token('test1')
        user.add_access_token('test2')
        user.add_access_token('test3')
        user.commit()
        self.assertEqual(3, Token.query.count())
        # When running notification_job
        cherrypy.token_cleanup.clean_up()
        # Then token are not removed
        self.assertEqual(3, Token.query.count())

    def test_clean_up_with_expired(self):
        # Given a user with 3 Token
        user = UserObject.get_user(self.USERNAME)
        user.add_access_token('test1')
        user.add_access_token('test2')
        user.add_access_token('test3')
        for t in Token.query.all():
            t.expiration_time = datetime.datetime.now()
        user.commit()
        self.assertEqual(3, Token.query.count())
        # When running notification_job
        cherrypy.token_cleanup.clean_up()
        # Then token are not removed
        self.assertEqual(0, Token.query.count())

    def test_add_access_token(self):
        # Given a user with an email
        userobj = UserObject.get_user(self.USERNAME)
        userobj.email = 'test@examples.com'
        userobj.commit()
        # When adding a new token
        token = userobj.add_access_token('test')
        userobj.commit()
        # Then a new token get created
        self.assertTrue(token)
        tokenobj = Token.query.filter(Token.userid == userobj.userid).first()
        self.assertTrue(tokenobj)
        self.assertEqual(None, tokenobj.expiration_time)
        self.assertEqual(None, tokenobj.access_time)
        # Then an email is sent to the user.
        self.listener.access_token_added.assert_called_once_with(userobj, 'test')
        self.listener.queue_mail.assert_called_once()

    def test_add_access_token_duplicate_name(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        userobj.add_access_token('test')
        userobj.commit()
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When adding a new token with the same name
        with self.assertRaises(ValueError):
            userobj.add_access_token('test')
            userobj.commit()
        userobj.rollback()
        # Then token is not created
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # Then an email is not sent.
        self.listener.access_token_added.assert_called_once_with(userobj, 'test')

    def test_delete_access_token(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        userobj.add_access_token('test')
        userobj.commit()
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When deleting an access token
        userobj.delete_access_token('test')
        userobj.commit()
        # Then Token get deleted
        self.assertEqual(0, Token.query.filter(Token.userid == userobj.userid).count())

    def test_delete_access_token_invalid(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        userobj.add_access_token('test')
        userobj.commit()
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When deleting an invalid access token
        with self.assertRaises(ValueError):
            userobj.delete_access_token('invalid')
        # Then Token not deleted
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())

    def test_delete_user_remove_access_tokens(self):
        # Given a user with an existing token
        userobj = UserObject.add_user('testuser', 'password')
        userobj.commit()
        userobj.add_access_token('test')
        userobj.commit()
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When deleting the user
        userobj.delete()
        userobj.commit()
        # Then Token get deleted
        self.assertEqual(0, Token.query.filter(Token.userid == userobj.userid).count())

    def test_verify_access_token(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        token = userobj.add_access_token('test')
        userobj.commit()
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When validating the token
        # Then token is valid
        self.assertTrue(userobj.validate_access_token(token))

    def test_verify_access_token_with_expired(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        token = userobj.add_access_token(
            'test', expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1)
        )
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When validating the token
        # Then token is invalid
        self.assertFalse(userobj.validate_access_token(token))
        # Then token is not removed
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())

    def test_verify_access_token_with_invalid(self):
        # Given a user with an existing token
        userobj = UserObject.get_user(self.USERNAME)
        userobj.add_access_token('test', expiration_time=datetime.datetime.now())
        userobj.commit()
        self.assertEqual(1, Token.query.filter(Token.userid == userobj.userid).count())
        # When validating the token
        # Then token is invalid
        self.assertFalse(userobj.validate_access_token('invalid'))
from rdiffweb.core.model import DuplicateSSHKeyError, RepoObject, UserObject
        userobj.commit()
        userobj.commit()
        user = UserObject.add_user('denise')
        user.commit()
        userobj.commit()
        user = UserObject.add_user('annik')
        user.commit()
        user.commit()
        user.commit()
        user.add().commit()
        user.commit()
        user.commit()
        user.commit()
        user.add().commit()
            user.add().commit()
        user.commit()
        user.commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
            userobj.commit()
        userobj.commit()
        userobj.commit()
        repos[1].commit()
        RepoObject(userid=userobj.userid, repopath='invalid').add().commit()
        userobj.commit()
        RepoObject(userid=userobj.userid, repopath='invalid').add().commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
        userobj = UserObject.add_user('jeff', 'password')
        userobj.commit()
        user.commit()
        repo.commit()
        user.add().commit()
        repo.add().commit()
        user.add().commit()
        repo.add().commit()
        user.add().commit()
        user.add().commit()
        user.add().commit()
        user.add().commit()
        user.add().commit()
        user.add().commit()
        user.add().commit()
        user.add().commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
        userobj.commit()
from rdiffweb.test import WebCase
class ListParentsTest(WebCase):
        user = UserObject.create_admin_user(cfg.admin_user, cfg.admin_password)
        user.commit()
    <p>{% trans %}A new access token, named "{{ name }}", has been created.{% endtrans %}</p>
        admin_user = UserObject.create_admin_user(self.USERNAME, self.PASSWORD)
        admin_user.commit()
            admin_user.commit()
        cherrypy.tools.db.drop_all()
from sqlalchemy import create_engine, event, inspect
from sqlalchemy.exc import SQLAlchemyError
def _get_model_changes(model):
    """
    Return a dictionary containing changes made to the model since it was
    fetched from the database.

    The dictionary is of the form {'property_name': [old_value, new_value]}
    """
    state = inspect(model)
    changes = {}
    for attr in state.attrs:
        hist = attr.history
        if not hist.has_changes():
            continue
        if isinstance(attr.value, (list, tuple)) or len(hist.deleted) > 1 or len(hist.added) > 1:
            # If array, store array
            changes[attr.key] = [hist.deleted, hist.added]
        else:
            # If primitive, store primitive
            changes[attr.key] = [
                hist.deleted[0] if len(hist.deleted) >= 1 else None,
                hist.added[0] if len(hist.added) >= 1 else None,
            ]
    return changes


    def add(self):
    def delete(self):
    def commit(self):
        self.__class__.session.commit()
        return self

    def flush(self):
        self.__class__.session.flush()
        return self

    def rollback(self):
        self.__class__.session.rollback()
        return self
        self.get_session().commit()
        self.get_session().commit()
            # When terminating, raise an error if objects are not commit.
            if self._session.dirty or self._session.new or self._session.deleted:
                changes = ', '.join([str(_get_model_changes(obj)) for obj in self._session.dirty])
                logger.exception(
                    'session is dirty, some database object(s) are not commited, this indicate a bug in the application '
                    'dirty %s new %s deleted %s' % (changes, self._session.new, self._session.deleted)
                )
                raise SQLAlchemyError('session is dirty')
        finally: