From db96d4b178e22a4db675ce2691f06f91dbd46322 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 20 Oct 2020 17:53:29 +0200 Subject: [PATCH] Made database datetime timezone aware --- flaschengeist/app.py | 1 + flaschengeist/modules/auth/__init__.py | 2 +- .../system/controller/sessionController.py | 11 +++--- flaschengeist/system/models/__init__.py | 35 +++++++++++++++++++ flaschengeist/system/models/session.py | 7 ++-- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/flaschengeist/app.py b/flaschengeist/app.py index 19b0254..bfe8cd3 100644 --- a/flaschengeist/app.py +++ b/flaschengeist/app.py @@ -21,6 +21,7 @@ class CustomJSONEncoder(JSONEncoder): if isinstance(o, datetime): return o.isoformat() + # Check if iterable try: iterable = iter(o) diff --git a/flaschengeist/modules/auth/__init__.py b/flaschengeist/modules/auth/__init__.py index 1c692fb..8f7a426 100644 --- a/flaschengeist/modules/auth/__init__.py +++ b/flaschengeist/modules/auth/__init__.py @@ -14,7 +14,7 @@ from flaschengeist.system.decorator import login_required from flaschengeist.system.controller import sessionController, userController, messageController from flaschengeist.system.models.session import Session -session_controller = LocalProxy(lambda: sessionController.SessionController()) +session_controller: sessionController.SessionController = LocalProxy(lambda: sessionController.SessionController()) auth_bp = Blueprint("auth", __name__) diff --git a/flaschengeist/system/controller/sessionController.py b/flaschengeist/system/controller/sessionController.py index 2f55b2f..b7607c9 100644 --- a/flaschengeist/system/controller/sessionController.py +++ b/flaschengeist/system/controller/sessionController.py @@ -36,7 +36,7 @@ class SessionController(metaclass=Singleton): access_token = Session.query.filter_by(token=token).one_or_none() if access_token: logger.debug("token found, check if expired or invalid user agent differs") - if access_token.expires >= datetime.utcnow() and ( + if access_token.expires >= datetime.now(timezone.utc) and ( access_token.browser == user_agent.browser and access_token.platform == user_agent.platform ): if not permissions or access_token.user.has_permissions(permissions): @@ -71,7 +71,6 @@ class SessionController(metaclass=Singleton): session.refresh() db.session.add(session) db.session.commit() - logger.debug("access token is {{ {} }}".format(session.token)) return session @@ -95,8 +94,7 @@ class SessionController(metaclass=Singleton): def get_users_sessions(self, user): return Session.query.filter(Session._user == user) - @staticmethod - def delete_session(token: Session): + def delete_session(self, token: Session): """Deletes given Session Args: @@ -105,8 +103,7 @@ class SessionController(metaclass=Singleton): db.session.delete(token) db.session.commit() - @staticmethod - def update_session(session): + def update_session(self, session): session.refresh() db.session.commit() @@ -117,6 +114,6 @@ class SessionController(metaclass=Singleton): def clear_expired(self): """Remove expired tokens from database""" logger.debug("Clear expired Sessions") - deleted = Session.query.filter(Session.expires < datetime.utcnow()).delete() + deleted = Session.query.filter(Session.expires < datetime.now(timezone.utc)).delete() logger.debug("{} sessions have been removed".format(deleted)) db.session.commit() diff --git a/flaschengeist/system/models/__init__.py b/flaschengeist/system/models/__init__.py index 6463684..2fd22e1 100644 --- a/flaschengeist/system/models/__init__.py +++ b/flaschengeist/system/models/__init__.py @@ -1,3 +1,7 @@ +import datetime +from sqlalchemy.types import DateTime, TypeDecorator + + class ModelSerializeMixin: def serialize(self): d = {param: getattr(self, param) for param in self.__class__.__annotations__ if not param.startswith("_")} @@ -5,3 +9,34 @@ class ModelSerializeMixin: key, value = d.popitem() return value return d + + +class UtcDateTime(TypeDecorator): + """Almost equivalent to :class:`~sqlalchemy.types.DateTime` with + ``timezone=True`` option, but it differs from that by: + - Never silently take naive :class:`~datetime.datetime`, instead it + always raise :exc:`ValueError` unless time zone aware value. + - :class:`~datetime.datetime` value's :attr:`~datetime.datetime.tzinfo` + is always converted to UTC. + - Unlike SQLAlchemy's built-in :class:`~sqlalchemy.types.DateTime`, + it never return naive :class:`~datetime.datetime`, but time zone + aware value, even with SQLite or MySQL. + """ + + impl = DateTime(timezone=True) + + def process_bind_param(self, value, dialect): + if value is not None: + if not isinstance(value, datetime.datetime): + raise TypeError('expected datetime.datetime, not ' + + repr(value)) + elif value.tzinfo is None: + raise ValueError('naive datetime is disallowed') + return value.astimezone(datetime.timezone.utc) + + def process_result_value(self, value, dialect): + if value is not None: + if value.tzinfo is not None: + value = value.astimezone(datetime.timezone.utc) + value = value.replace(tzinfo=datetime.timezone.utc) + return value diff --git a/flaschengeist/system/models/session.py b/flaschengeist/system/models/session.py index 488fe46..a17ba8b 100644 --- a/flaschengeist/system/models/session.py +++ b/flaschengeist/system/models/session.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta, timezone -from . import ModelSerializeMixin +from . import ModelSerializeMixin, UtcDateTime from .user import User from ..database import db from secrets import compare_digest @@ -17,7 +17,7 @@ class Session(db.Model, ModelSerializeMixin): """ __tablename__ = "session" - expires: datetime = db.Column(db.DateTime) + expires: datetime = db.Column(UtcDateTime) token: str = db.Column(db.String(32), unique=True) lifetime: int = db.Column(db.Integer) browser: str = db.Column(db.String(30)) @@ -33,8 +33,7 @@ class Session(db.Model, ModelSerializeMixin): Update the Timestamp to the current Time. """ logger.debug("update timestamp from session with token {{ {} }}".format(self)) - self.expires = datetime.utcnow() + timedelta(seconds=self.lifetime) - self.expires.replace(tzinfo=timezone.utc) + self.expires = datetime.now(timezone.utc) + timedelta(seconds=self.lifetime) def __eq__(self, token): return compare_digest(self.token, token)