From ffcec8a49f4805b9f640a291600a03d880c9ab41 Mon Sep 17 00:00:00 2001 From: Julian Rother <julian@cccv.de> Date: Tue, 16 Aug 2022 15:25:08 +0200 Subject: [PATCH] Use UTC internally Convert DateTime fields to UTC, use "utcnow" instead of "now" and use babel helper/filter when dates/times are displayed or parsed from user input. Uffd continues to use the system's timezone in the user interface by default. However, it is now possible to overwrite this with the BABEL_DEFAULT_TIMEZONE config option. --- tests/test_invite.py | 118 +++++++++--------- tests/test_mfa.py | 2 +- tests/test_selfservice.py | 4 +- tests/test_signup.py | 8 +- uffd/__init__.py | 17 ++- .../9f824f61d8ac_use_utc_for_datetime.py | 111 ++++++++++++++++ uffd/models/invite.py | 4 +- uffd/models/mfa.py | 4 +- uffd/models/selfservice.py | 8 +- uffd/models/session.py | 4 +- uffd/models/signup.py | 4 +- uffd/templates/invite/list.html | 8 +- uffd/templates/mfa/setup.html | 4 +- uffd/views/invite.py | 9 +- uffd/views/session.py | 4 +- 15 files changed, 219 insertions(+), 90 deletions(-) create mode 100644 uffd/migrations/versions/9f824f61d8ac_use_utc_for_datetime.py diff --git a/tests/test_invite.py b/tests/test_invite.py index 29510320..93dea192 100644 --- a/tests/test_invite.py +++ b/tests/test_invite.py @@ -12,21 +12,21 @@ from utils import dump, UffdTestCase, db_flush class TestInviteModel(UffdTestCase): def test_expire(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), creator=self.get_admin()) self.assertFalse(invite.expired) self.assertTrue(invite.active) - invite.valid_until = datetime.datetime.now() - datetime.timedelta(seconds=60) + invite.valid_until = datetime.datetime.utcnow() - datetime.timedelta(seconds=60) self.assertTrue(invite.expired) self.assertFalse(invite.active) def test_void(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), single_use=False, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), single_use=False, creator=self.get_admin()) self.assertFalse(invite.voided) self.assertTrue(invite.active) invite.used = True self.assertFalse(invite.voided) self.assertTrue(invite.active) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), single_use=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), single_use=True, creator=self.get_admin()) self.assertFalse(invite.voided) self.assertTrue(invite.active) invite.used = True @@ -35,7 +35,7 @@ class TestInviteModel(UffdTestCase): def test_permitted(self): role = Role(name='testrole') - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, roles=[role]) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, roles=[role]) self.assertFalse(invite.permitted) self.assertFalse(invite.active) invite.creator = self.get_admin() @@ -57,26 +57,26 @@ class TestInviteModel(UffdTestCase): self.assertFalse(invite.active) def test_disable(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), creator=self.get_admin()) self.assertTrue(invite.active) invite.disable() self.assertFalse(invite.active) def test_reset_disabled(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), creator=self.get_admin()) invite.disable() self.assertFalse(invite.active) invite.reset() self.assertTrue(invite.active) def test_reset_expired(self): - invite = Invite(valid_until=datetime.datetime.now() - datetime.timedelta(seconds=60), creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() - datetime.timedelta(seconds=60), creator=self.get_admin()) self.assertFalse(invite.active) invite.reset() self.assertFalse(invite.active) def test_reset_single_use(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), single_use=False, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), single_use=False, creator=self.get_admin()) invite.used = True invite.disable() self.assertFalse(invite.active) @@ -84,7 +84,7 @@ class TestInviteModel(UffdTestCase): self.assertTrue(invite.active) def test_short_token(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), creator=self.get_admin()) db.session.add(invite) db.session.commit() self.assertTrue(len(invite.short_token) <= len(invite.token)/3) @@ -102,7 +102,7 @@ class TestInviteGrantModel(UffdTestCase): db.session.add(role1) role2 = Role(name='testrole2') db.session.add(role2) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), roles=[role1, role2], creator=self.get_admin()) self.assertIn(role0, user.roles) self.assertNotIn(role1, user.roles) self.assertNotIn(role2, user.roles) @@ -132,7 +132,7 @@ class TestInviteGrantModel(UffdTestCase): group = self.get_admin_group() role = Role(name='testrole1', groups={group: RoleGroup(group=group)}) db.session.add(role) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role], single_use=True, used=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), roles=[role], single_use=True, used=True, creator=self.get_admin()) self.assertFalse(invite.active) grant = InviteGrant(invite=invite, user=user) success, msg = grant.apply() @@ -143,7 +143,7 @@ class TestInviteGrantModel(UffdTestCase): def test_no_roles(self): user = self.get_user() - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), creator=self.get_admin()) self.assertTrue(invite.active) grant = InviteGrant(invite=invite, user=user) success, msg = grant.apply() @@ -155,7 +155,7 @@ class TestInviteGrantModel(UffdTestCase): role = Role(name='testrole1') db.session.add(role) user.roles.append(role) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role], creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), roles=[role], creator=self.get_admin()) self.assertTrue(invite.active) grant = InviteGrant(invite=invite, user=user) success, msg = grant.apply() @@ -180,7 +180,7 @@ class TestInviteSignupModel(UffdTestCase): db.session.add(role1) role2 = Role(name='testrole2') db.session.add(role2) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), roles=[role1, role2], allow_signup=True, creator=self.get_admin()) signup = InviteSignup(invite=invite, loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') self.assertFalse(invite.used) valid, msg = signup.validate() @@ -208,7 +208,7 @@ class TestInviteSignupModel(UffdTestCase): base_role = Role.query.filter_by(name='base').one() base_group1 = self.get_access_group() base_group2 = self.get_users_group() - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) signup = InviteSignup(invite=invite, loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') self.assertFalse(invite.used) valid, msg = signup.validate() @@ -231,7 +231,7 @@ class TestInviteSignupModel(UffdTestCase): self.assertEqual(len(User.query.filter_by(loginname='newuser').all()), 1) def test_inactive(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, single_use=True, used=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, single_use=True, used=True, creator=self.get_admin()) self.assertFalse(invite.active) signup = InviteSignup(invite=invite, loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') valid, msg = signup.validate() @@ -242,7 +242,7 @@ class TestInviteSignupModel(UffdTestCase): self.assertIsInstance(msg, str) def test_invalid(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) self.assertTrue(invite.active) signup = InviteSignup(invite=invite, loginname='', displayname='New User', mail='test@example.com', password='notsecret') valid, msg = signup.validate() @@ -250,7 +250,7 @@ class TestInviteSignupModel(UffdTestCase): self.assertIsInstance(msg, str) def test_invalid2(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) self.assertTrue(invite.active) signup = InviteSignup(invite=invite, loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') user, msg = signup.finish('wrongpassword') @@ -258,7 +258,7 @@ class TestInviteSignupModel(UffdTestCase): self.assertIsInstance(msg, str) def test_no_signup(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=False, creator=self.get_admin()) self.assertTrue(invite.active) signup = InviteSignup(invite=invite, loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') valid, msg = signup.validate() @@ -274,8 +274,8 @@ class TestInviteAdminViews(UffdTestCase): self.app.last_mail = None def test_index(self): - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) - valid_until_expired = datetime.datetime.now() - datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) + valid_until_expired = datetime.datetime.utcnow() - datetime.timedelta(seconds=60) user1 = self.get_user() user2 = self.get_admin() role1 = Role(name='testrole1') @@ -320,7 +320,7 @@ class TestInviteAdminViews(UffdTestCase): def test_index_signupperm(self): current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_access' - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) invite1 = Invite(valid_until=valid_until, allow_signup=True, creator=self.get_admin()) db.session.add(invite1) invite2 = Invite(valid_until=valid_until, allow_signup=True, creator=self.get_user()) @@ -343,7 +343,7 @@ class TestInviteAdminViews(UffdTestCase): db.session.add(role1) role2 = Role(name='testrole2', moderator_group=self.get_access_group()) db.session.add(role2) - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) db.session.add(Invite(valid_until=valid_until, roles=[role1])) db.session.add(Invite(valid_until=valid_until, roles=[role2])) db.session.commit() @@ -362,10 +362,10 @@ class TestInviteAdminViews(UffdTestCase): r = self.client.get(path=url_for('invite.new'), follow_redirects=True) dump('invite_new', r) self.assertEqual(r.status_code, 200) - valid_until = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat(timespec='minutes') + valid_until_input = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat(timespec='minutes') self.assertListEqual(Invite.query.all(), []) r = self.client.post(path=url_for('invite.new_submit'), - data={'single-use': '1', 'valid-until': valid_until, + data={'single-use': '1', 'valid-until': valid_until_input, 'allow-signup': '1', 'role-%d'%role_id: '1'}, follow_redirects=True) dump('invite_new_submit', r) invite = Invite.query.one() @@ -382,9 +382,9 @@ class TestInviteAdminViews(UffdTestCase): db.session.add(role) db.session.commit() role_id = role.id - valid_until = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat(timespec='minutes') + valid_until_input = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat(timespec='minutes') r = self.client.post(path=url_for('invite.new_submit'), - data={'single-use': '1', 'valid-until': valid_until, + data={'single-use': '1', 'valid-until': valid_until_input, 'allow-signup': '1', 'role-%d'%role_id: '1'}, follow_redirects=True) dump('invite_new_noperm', r) self.assertIsNone(Invite.query.first()) @@ -392,16 +392,16 @@ class TestInviteAdminViews(UffdTestCase): def test_new_empty(self): current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_access' self.login_as('user') - valid_until = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat(timespec='minutes') + valid_until_input = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat(timespec='minutes') r = self.client.post(path=url_for('invite.new_submit'), - data={'single-use': '1', 'valid-until': valid_until, + data={'single-use': '1', 'valid-until': valid_until_input, 'allow-signup': '0'}, follow_redirects=True) dump('invite_new_empty', r) self.assertIsNone(Invite.query.first()) def test_disable(self): self.login_as('admin') - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) invite = Invite(valid_until=valid_until, creator=self.get_admin()) db.session.add(invite) db.session.commit() @@ -414,7 +414,7 @@ class TestInviteAdminViews(UffdTestCase): def test_disable_own(self): current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_access' self.login_as('user') - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) invite = Invite(valid_until=valid_until, creator=self.get_user()) db.session.add(invite) db.session.commit() @@ -426,7 +426,7 @@ class TestInviteAdminViews(UffdTestCase): def test_disable_rolemod(self): self.login_as('user') - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) role = Role(name='testrole', moderator_group=self.get_access_group()) db.session.add(role) invite = Invite(valid_until=valid_until, creator=self.get_admin(), roles=[role]) @@ -439,7 +439,7 @@ class TestInviteAdminViews(UffdTestCase): def test_disable_noperm(self): self.login_as('user') - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) db.session.add(Role(name='testrole1', moderator_group=self.get_access_group())) role = Role(name='testrole2', moderator_group=self.get_admin_group()) db.session.add(role) @@ -453,7 +453,7 @@ class TestInviteAdminViews(UffdTestCase): def test_reset_disabled(self): self.login_as('admin') - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) invite = Invite(valid_until=valid_until, disabled=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() @@ -465,7 +465,7 @@ class TestInviteAdminViews(UffdTestCase): def test_reset_voided(self): self.login_as('admin') - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) invite = Invite(valid_until=valid_until, single_use=True, used=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() @@ -478,7 +478,7 @@ class TestInviteAdminViews(UffdTestCase): def test_reset_own(self): current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_access' self.login_as('user') - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) invite = Invite(valid_until=valid_until, disabled=True, creator=self.get_user()) db.session.add(invite) db.session.commit() @@ -490,7 +490,7 @@ class TestInviteAdminViews(UffdTestCase): def test_reset_foreign(self): self.login_as('user') - valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) + valid_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=60) role = Role(name='testrole', moderator_group=self.get_access_group()) db.session.add(role) invite = Invite(valid_until=valid_until, disabled=True, creator=self.get_admin(), roles=[role]) @@ -507,7 +507,7 @@ class TestInviteUseViews(UffdTestCase): self.app.last_mail = None def test_use(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, roles=[Role(name='testrole1'), Role(name='testrole2')]) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, roles=[Role(name='testrole1'), Role(name='testrole2')]) db.session.add(invite) db.session.commit() token = invite.token @@ -517,7 +517,7 @@ class TestInviteUseViews(UffdTestCase): def test_use_loggedin(self): self.login_as('user') - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, roles=[Role(name='testrole1'), Role(name='testrole2')]) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, roles=[Role(name='testrole1'), Role(name='testrole2')]) db.session.add(invite) db.session.commit() token = invite.token @@ -526,7 +526,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_use_inactive(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), disabled=True) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), disabled=True) db.session.add(invite) db.session.commit() token = invite.token @@ -548,7 +548,7 @@ class TestInviteUseViews(UffdTestCase): db.session.add(role1) role2 = Role(name='testrole2') db.session.add(role2) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), roles=[role1, role2], creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -574,7 +574,7 @@ class TestInviteUseViews(UffdTestCase): self.assertIn(self.get_admin_group(), user.groups) def test_grant_invalid_invite(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), disabled=True) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), disabled=True) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -586,7 +586,7 @@ class TestInviteUseViews(UffdTestCase): self.assertFalse(Invite.query.filter_by(token=token).first().used) def test_grant_no_roles(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60)) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60)) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -602,7 +602,7 @@ class TestInviteUseViews(UffdTestCase): role = Role(name='testrole') db.session.add(role) user.roles.append(role) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role]) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), roles=[role]) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -623,7 +623,7 @@ class TestInviteUseViews(UffdTestCase): db.session.add(role1) role2 = Role(name='testrole2') db.session.add(role2) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), roles=[role1, role2], allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -645,7 +645,7 @@ class TestInviteUseViews(UffdTestCase): self.assertTrue(signup.validate()[0]) def test_signup_invalid_invite(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, disabled=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, disabled=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() r = self.client.get(path=url_for('invite.signup_start', invite_id=invite.id, token=invite.token), follow_redirects=True) @@ -653,7 +653,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_signup_nosignup(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=False, creator=self.get_admin()) db.session.add(invite) db.session.commit() r = self.client.get(path=url_for('invite.signup_start', invite_id=invite.id, token=invite.token), follow_redirects=True) @@ -661,7 +661,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_signup_wrongpassword(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite.id, token=invite.token), @@ -671,7 +671,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_signup_invalid(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite.id, token=invite.token), @@ -682,7 +682,7 @@ class TestInviteUseViews(UffdTestCase): def test_signup_mailerror(self): self.app.config['MAIL_SKIP_SEND'] = 'fail' - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite.id, token=invite.token), @@ -692,7 +692,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_signup_hostlimit(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -712,7 +712,7 @@ class TestInviteUseViews(UffdTestCase): self.assertIsNone(self.app.last_mail) def test_signup_mailimit(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -732,7 +732,7 @@ class TestInviteUseViews(UffdTestCase): self.assertIsNone(self.app.last_mail) def test_signup_check(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -744,7 +744,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.json['status'], 'ok') def test_signup_check_invalid(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -757,7 +757,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.json['status'], 'invalid') def test_signup_check_exists(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -769,7 +769,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.json['status'], 'exists') def test_signup_check_nosignup(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=False, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -781,7 +781,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.json['status'], 'error') def test_signup_check_error(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, disabled=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, disabled=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -793,7 +793,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.json['status'], 'error') def test_signup_check_ratelimited(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) + invite = Invite(valid_until=datetime.datetime.utcnow() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id diff --git a/tests/test_mfa.py b/tests/test_mfa.py index ff32a105..f1e254a3 100644 --- a/tests/test_mfa.py +++ b/tests/test_mfa.py @@ -31,7 +31,7 @@ def get_fido2_test_cred(self): class TestMfaMethodModels(UffdTestCase): def test_common_attributes(self): method = TOTPMethod(user=self.get_user(), name='testname') - self.assertTrue(method.created <= datetime.datetime.now()) + self.assertTrue(method.created <= datetime.datetime.utcnow()) self.assertEqual(method.name, 'testname') self.assertEqual(method.user.loginname, 'testuser') method.user = self.get_admin() diff --git a/tests/test_selfservice.py b/tests/test_selfservice.py index cc2b0a86..760b7a44 100644 --- a/tests/test_selfservice.py +++ b/tests/test_selfservice.py @@ -186,7 +186,7 @@ class TestSelfservice(UffdTestCase): user = request.user old_mail = user.mail token = MailToken(user=user, newmail='newusermail@example.com', - created=(datetime.datetime.now() - datetime.timedelta(days=10))) + created=(datetime.datetime.utcnow() - datetime.timedelta(days=10))) db.session.add(token) db.session.commit() r = self.client.get(path=url_for('selfservice.token_mail', token_id=token.id, token=token.token), follow_redirects=True) @@ -288,7 +288,7 @@ class TestSelfservice(UffdTestCase): def test_token_password_expired(self): user = self.get_user() - token = PasswordToken(user=user, created=(datetime.datetime.now() - datetime.timedelta(days=10))) + token = PasswordToken(user=user, created=(datetime.datetime.utcnow() - datetime.timedelta(days=10))) db.session.add(token) db.session.commit() self.assertTrue(token.expired) diff --git a/tests/test_signup.py b/tests/test_signup.py index e77c7c46..cfb599d5 100644 --- a/tests/test_signup.py +++ b/tests/test_signup.py @@ -61,7 +61,7 @@ class TestSignupModel(UffdTestCase): # TODO: Find a better way to test this! signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') self.assertFalse(signup.expired) - signup.created = created=datetime.datetime.now() - datetime.timedelta(hours=49) + signup.created = created=datetime.datetime.utcnow() - datetime.timedelta(hours=49) self.assertTrue(signup.expired) def test_completed(self): @@ -86,7 +86,7 @@ class TestSignupModel(UffdTestCase): def test_validate_expired(self): signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', - password='notsecret', created=datetime.datetime.now()-datetime.timedelta(hours=49)) + password='notsecret', created=datetime.datetime.utcnow()-datetime.timedelta(hours=49)) self.assert_validate_invalid(signup) self.assert_validate_invalid(refetch_signup(signup)) @@ -132,7 +132,7 @@ class TestSignupModel(UffdTestCase): def test_finish_expired(self): # TODO: Find a better way to test this! signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', - password='notsecret', created=datetime.datetime.now()-datetime.timedelta(hours=49)) + password='notsecret', created=datetime.datetime.utcnow()-datetime.timedelta(hours=49)) self.assert_finish_failure(signup, 'notsecret') self.assert_finish_failure(refetch_signup(signup), 'notsecret') @@ -366,7 +366,7 @@ class TestSignupViews(UffdTestCase): def test_confirm_expired(self): signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') - signup.created = datetime.datetime.now() - datetime.timedelta(hours=49) + signup.created = datetime.datetime.utcnow() - datetime.timedelta(hours=49) signup = refetch_signup(signup) r = self.client.get(path=url_for('signup.signup_confirm', signup_id=signup.id, token=signup.token), follow_redirects=True) dump('test_signup_confirm_expired', r) diff --git a/uffd/__init__.py b/uffd/__init__.py index d178c85f..ad186dee 100644 --- a/uffd/__init__.py +++ b/uffd/__init__.py @@ -4,6 +4,7 @@ import sys from flask import Flask, redirect, url_for, request, render_template from flask_babel import Babel +from babel.dates import LOCALTZ from werkzeug.exceptions import Forbidden from flask_migrate import Migrate @@ -86,7 +87,21 @@ def create_app(test_config=None): # pylint: disable=too-many-locals,too-many-sta def push_request_context(): #pylint: disable=unused-variable return {'db': db} | {name: getattr(models, name) for name in models.__all__} - babel = Babel(app) + # flask-babel requires pytz-style timezone objects, but in rare cases (e.g. + # non-IANA TZ values) LOCALTZ is stdlib-style (without normalize/localize) + if not hasattr(LOCALTZ, 'normalize'): + LOCALTZ.normalize = lambda dt: dt + if not hasattr(LOCALTZ, 'localize'): + LOCALTZ.localize = lambda dt: dt.replace(tzinfo=LOCALTZ) + + class PatchedBabel(Babel): + @property + def default_timezone(self): + if self.app.config['BABEL_DEFAULT_TIMEZONE'] == 'LOCALTZ': + return LOCALTZ + return super().default_timezone + + babel = PatchedBabel(app, default_timezone='LOCALTZ') @babel.localeselector def get_locale(): #pylint: disable=unused-variable diff --git a/uffd/migrations/versions/9f824f61d8ac_use_utc_for_datetime.py b/uffd/migrations/versions/9f824f61d8ac_use_utc_for_datetime.py new file mode 100644 index 00000000..f5290617 --- /dev/null +++ b/uffd/migrations/versions/9f824f61d8ac_use_utc_for_datetime.py @@ -0,0 +1,111 @@ +"""Use UTC for DateTime + +Revision ID: 9f824f61d8ac +Revises: a60ce68b9214 +Create Date: 2022-08-16 00:51:04.635182 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '9f824f61d8ac' +down_revision = 'a60ce68b9214' +branch_labels = None +depends_on = None + +import datetime + +def localtime_to_utc(dt): + return dt.astimezone(datetime.timezone.utc).replace(tzinfo=None) + +def utc_to_localtime(dt): + return dt.replace(tzinfo=datetime.timezone.utc).astimezone().replace(tzinfo=None) + +def iter_rows_paged(table, pk='id', limit=1000): + conn = op.get_bind() + pk_column = getattr(table.c, pk) + last_pk = None + while True: + expr = table.select().order_by(pk_column).limit(limit) + if last_pk is not None: + expr = expr.where(pk_column > last_pk) + result = conn.execute(expr) + pk_index = result.keys().index(pk) + rows = result.fetchall() + if not rows: + break + yield from rows + last_pk = rows[-1][pk_index] + +invite = sa.table('invite', + sa.column('id', sa.Integer), + sa.column('created', sa.DateTime), + sa.column('valid_until', sa.DateTime), +) + +password_token = sa.table('passwordToken', + sa.column('id', sa.Integer), + sa.column('created', sa.DateTime), +) + +mail_token = sa.table('mailToken', + sa.column('id', sa.Integer), + sa.column('created', sa.DateTime), +) + +device_login_initiation = sa.table('device_login_initiation', + sa.column('id', sa.Integer), + sa.column('created', sa.DateTime), +) + +signup = sa.table('signup', + sa.column('id', sa.Integer), + sa.column('created', sa.DateTime), +) + +def upgrade(): + for obj_id, created, valid_until in iter_rows_paged(invite): + op.execute(invite.update().where(invite.c.id==obj_id).values( + created=localtime_to_utc(created), + valid_until=localtime_to_utc(valid_until), + )) + for obj_id, created in iter_rows_paged(password_token): + op.execute(password_token.update().where(password_token.c.id==obj_id).values( + created=localtime_to_utc(created), + )) + for obj_id, created in iter_rows_paged(mail_token): + op.execute(mail_token.update().where(mail_token.c.id==obj_id).values( + created=localtime_to_utc(created), + )) + for obj_id, created in iter_rows_paged(device_login_initiation): + op.execute(device_login_initiation.update().where(device_login_initiation.c.id==obj_id).values( + created=localtime_to_utc(created), + )) + for obj_id, created in iter_rows_paged(signup): + op.execute(signup.update().where(signup.c.id==obj_id).values( + created=localtime_to_utc(created), + )) + +def downgrade(): + for obj_id, created, valid_until in iter_rows_paged(invite): + op.execute(invite.update().where(invite.c.id==obj_id).values( + created=utc_to_localtime(created), + valid_until=utc_to_localtime(valid_until), + )) + for obj_id, created in iter_rows_paged(password_token): + op.execute(password_token.update().where(password_token.c.id==obj_id).values( + created=utc_to_localtime(created), + )) + for obj_id, created in iter_rows_paged(mail_token): + op.execute(mail_token.update().where(mail_token.c.id==obj_id).values( + created=utc_to_localtime(created), + )) + for obj_id, created in iter_rows_paged(device_login_initiation): + op.execute(device_login_initiation.update().where(device_login_initiation.c.id==obj_id).values( + created=utc_to_localtime(created), + )) + for obj_id, created in iter_rows_paged(signup): + op.execute(signup.update().where(signup.c.id==obj_id).values( + created=utc_to_localtime(created), + )) diff --git a/uffd/models/invite.py b/uffd/models/invite.py index 12018d35..ad550b25 100644 --- a/uffd/models/invite.py +++ b/uffd/models/invite.py @@ -18,7 +18,7 @@ class Invite(db.Model): __tablename__ = 'invite' id = Column(Integer(), primary_key=True, autoincrement=True) token = Column(String(128), unique=True, nullable=False, default=token_urlfriendly) - created = Column(DateTime, default=datetime.datetime.now, nullable=False) + created = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) creator_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE'), nullable=True) creator = relationship('User') valid_until = Column(DateTime, nullable=False) @@ -32,7 +32,7 @@ class Invite(db.Model): @property def expired(self): - return datetime.datetime.now().replace(second=0, microsecond=0) > self.valid_until + return datetime.datetime.utcnow().replace(second=0, microsecond=0) > self.valid_until @property def voided(self): diff --git a/uffd/models/mfa.py b/uffd/models/mfa.py index 7db21b5c..9d2b3458 100644 --- a/uffd/models/mfa.py +++ b/uffd/models/mfa.py @@ -29,7 +29,7 @@ class MFAMethod(db.Model): __tablename__ = 'mfa_method' id = Column(Integer(), primary_key=True, autoincrement=True) type = Column(Enum(MFAType), nullable=False) - created = Column(DateTime(), nullable=False, default=datetime.datetime.now) + created = Column(DateTime(), nullable=False, default=datetime.datetime.utcnow) name = Column(String(128)) user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) user = relationship('User', backref=backref('mfa_methods', cascade='all, delete-orphan')) @@ -41,7 +41,7 @@ class MFAMethod(db.Model): def __init__(self, user, name=None): self.user = user self.name = name - self.created = datetime.datetime.now() + self.created = datetime.datetime.utcnow() class RecoveryCodeMethod(MFAMethod): code_salt = Column('recovery_salt', String(64)) diff --git a/uffd/models/selfservice.py b/uffd/models/selfservice.py index bb148457..5c4d5eb1 100644 --- a/uffd/models/selfservice.py +++ b/uffd/models/selfservice.py @@ -13,7 +13,7 @@ class PasswordToken(db.Model): __tablename__ = 'passwordToken' id = Column(Integer(), primary_key=True, autoincrement=True) token = Column(String(128), default=token_urlfriendly, nullable=False) - created = Column(DateTime, default=datetime.datetime.now, nullable=False) + created = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) user = relationship('User') @@ -21,14 +21,14 @@ class PasswordToken(db.Model): def expired(self): if self.created is None: return False - return self.created < datetime.datetime.now() - datetime.timedelta(days=2) + return self.created < datetime.datetime.utcnow() - datetime.timedelta(days=2) @cleanup_task.delete_by_attribute('expired') class MailToken(db.Model): __tablename__ = 'mailToken' id = Column(Integer(), primary_key=True, autoincrement=True) token = Column(String(128), default=token_urlfriendly, nullable=False) - created = Column(DateTime, default=datetime.datetime.now) + created = Column(DateTime, default=datetime.datetime.utcnow) user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) user = relationship('User') newmail = Column(String(255)) @@ -37,4 +37,4 @@ class MailToken(db.Model): def expired(self): if self.created is None: return False - return self.created < datetime.datetime.now() - datetime.timedelta(days=2) + return self.created < datetime.datetime.utcnow() - datetime.timedelta(days=2) diff --git a/uffd/models/session.py b/uffd/models/session.py index 6b72912b..19fa3ad8 100644 --- a/uffd/models/session.py +++ b/uffd/models/session.py @@ -80,7 +80,7 @@ class DeviceLoginInitiation(db.Model): code1 = Column(String(32), unique=True, nullable=False, default=lambda: token_typeable(3)) secret = Column(String(128), nullable=False, default=lambda: secrets.token_hex(64)) confirmations = relationship('DeviceLoginConfirmation', back_populates='initiation', cascade='all, delete-orphan') - created = Column(DateTime, default=datetime.datetime.now, nullable=False) + created = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) __mapper_args__ = { 'polymorphic_on': type, @@ -96,7 +96,7 @@ class DeviceLoginInitiation(db.Model): def expired(self): if self.created is None: return False - return self.created < datetime.datetime.now() - datetime.timedelta(minutes=30) + return self.created < datetime.datetime.utcnow() - datetime.timedelta(minutes=30) @property def description(self): diff --git a/uffd/models/signup.py b/uffd/models/signup.py index 204edbc4..c04dbb41 100644 --- a/uffd/models/signup.py +++ b/uffd/models/signup.py @@ -31,7 +31,7 @@ class Signup(db.Model): __tablename__ = 'signup' id = Column(Integer(), primary_key=True, autoincrement=True) token = Column(String(128), default=token_urlfriendly, nullable=False) - created = Column(DateTime, default=datetime.datetime.now, nullable=False) + created = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) loginname = Column(Text) displayname = Column(Text) mail = Column(Text) @@ -56,7 +56,7 @@ class Signup(db.Model): def expired(self): if self.created is None: return False - return self.created < datetime.datetime.now() - datetime.timedelta(hours=48) + return self.created < datetime.datetime.utcnow() - datetime.timedelta(hours=48) @hybrid_property def completed(self): diff --git a/uffd/templates/invite/list.html b/uffd/templates/invite/list.html index 9a179a87..97c71623 100644 --- a/uffd/templates/invite/list.html +++ b/uffd/templates/invite/list.html @@ -56,9 +56,9 @@ {% elif not invite.active %} {{_('Invalid')}} {% elif invite.single_use %} - {{ _('Valid once, expires %(expiry_date)s', expiry_date=invite.valid_until.strftime('%Y-%m-%d')) }} + {{ _('Valid once, expires %(expiry_date)s', expiry_date=invite.valid_until|dateformat) }} {% else %} - {{ _('Valid, expires %(expiry_date)s',expiry_date=invite.valid_until.strftime('%Y-%m-%d')) }} + {{ _('Valid, expires %(expiry_date)s', expiry_date=invite.valid_until|dateformat) }} {% endif %} </td> <td class="text-right"> @@ -83,8 +83,8 @@ <div class="modal-body"> <ul class="list-unstyled"> <li><b>{{_('Type:')}}</b> {% if invite.single_use %}{{_('Single-use')}}{% else %}{{_('Multi-use')}}{% endif %}</li> - <li><b>{{_('Created:')}}</b> {{ invite.created.strftime('%Y-%m-%d %H:%M:%S') }}</li> - <li><b>{{_('Expires:')}}</b> {{ invite.valid_until.strftime('%Y-%m-%d %H:%M:%S') }}</li> + <li><b>{{_('Created:')}}</b> {{ invite.created|datetimeformat }}</li> + <li><b>{{_('Expires:')}}</b> {{ invite.valid_until|datetimeformat }}</li> <li><b>{{_('Permissions:')}}</b> <ul> {% if invite.allow_signup %} diff --git a/uffd/templates/mfa/setup.html b/uffd/templates/mfa/setup.html index 5d5aefaa..2922aac2 100644 --- a/uffd/templates/mfa/setup.html +++ b/uffd/templates/mfa/setup.html @@ -113,7 +113,7 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe {% for method in request.user.mfa_totp_methods %} <tr> <td>{{ method.name }}</td> - <td>{{ method.created.strftime('%b %d, %Y') }}</td> + <td>{{ method.created|dateformat }}</td> <td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_totp', id=method.id) }}">{{_("Delete")}}</a></td> </tr> {% endfor %} @@ -175,7 +175,7 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe {% for method in request.user.mfa_webauthn_methods %} <tr> <td>{{ method.name }}</td> - <td>{{ method.created.strftime('%b %d, %Y') }}</td> + <td>{{ method.created|dateformat }}</td> <td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_webauthn', id=method.id) }}">{{_("Delete")}}</a></td> </tr> {% endfor %} diff --git a/uffd/views/invite.py b/uffd/views/invite.py index 99b0675e..d05848b3 100644 --- a/uffd/views/invite.py +++ b/uffd/views/invite.py @@ -2,7 +2,7 @@ import datetime import secrets from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify, abort -from flask_babel import gettext as _, lazy_gettext +from flask_babel import gettext as _, lazy_gettext, to_utc import sqlalchemy from uffd.csrf import csrf_protect @@ -57,18 +57,21 @@ def new(): roles = Role.query.join(Role.moderator_group).join(Group.members).filter(User.id==request.user.id).all() return render_template('invite/new.html', roles=roles, allow_signup=allow_signup) +def parse_datetime_local_input(value): + return to_utc(datetime.datetime.fromisoformat(value)) + @bp.route('/new', methods=['POST']) @login_required(invite_acl_check) @csrf_protect(blueprint=bp) def new_submit(): invite = Invite(creator=request.user, single_use=(request.values['single-use'] == '1'), - valid_until=datetime.datetime.fromisoformat(request.values['valid-until']), + valid_until=parse_datetime_local_input(request.values['valid-until']), allow_signup=(request.values.get('allow-signup', '0') == '1')) for key, value in request.values.items(): if key.startswith('role-') and value == '1': invite.roles.append(Role.query.get(key[5:])) - if invite.valid_until > datetime.datetime.now() + datetime.timedelta(days=current_app.config['INVITE_MAX_VALID_DAYS']): + if invite.valid_until > datetime.datetime.utcnow() + datetime.timedelta(days=current_app.config['INVITE_MAX_VALID_DAYS']): flash(_('The "Expires After" date is too far in the future')) return new() if not invite.permitted: diff --git a/uffd/views/session.py b/uffd/views/session.py index 3f6a6187..9e45c210 100644 --- a/uffd/views/session.py +++ b/uffd/views/session.py @@ -22,7 +22,7 @@ def set_request_user(): return if 'logintime' not in session: return - if datetime.datetime.now().timestamp() > session['logintime'] + current_app.config['SESSION_LIFETIME_SECONDS']: + if datetime.datetime.utcnow().timestamp() > session['logintime'] + current_app.config['SESSION_LIFETIME_SECONDS']: return user = User.query.get(session['user_id']) if not user or not user.is_in_group(current_app.config['ACL_ACCESS_GROUP']): @@ -49,7 +49,7 @@ def set_session(user, skip_mfa=False): session.clear() session.permanent = True session['user_id'] = user.id - session['logintime'] = datetime.datetime.now().timestamp() + session['logintime'] = datetime.datetime.utcnow().timestamp() session['_csrf_token'] = secrets.token_hex(128) if skip_mfa: session['user_mfa'] = True -- GitLab