Skip to content
This repository has been archived by the owner on Apr 28, 2020. It is now read-only.

Commit

Permalink
Switch to timestamptz
Browse files Browse the repository at this point in the history
  • Loading branch information
jace committed May 9, 2019
1 parent b2dd017 commit 5ea3423
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 42 deletions.
1 change: 1 addition & 0 deletions lastuser_core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from coaster.sqlalchemy import TimestampMixin, BaseMixin, BaseScopedNameMixin, UuidMixin # NOQA
from coaster.db import db

TimestampMixin.__with_timezone__ = True

from .user import * # NOQA
from .session import * # NOQA
Expand Down
10 changes: 5 additions & 5 deletions lastuser_core/models/client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-

from datetime import datetime, timedelta
from datetime import timedelta
import urlparse
from hashlib import sha256
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import load_only
from sqlalchemy.orm.query import Query as QueryBaseClass
from sqlalchemy.orm.collections import attribute_mapped_collection
from coaster.utils import buid, newsecret, require_one_of
from coaster.utils import buid, newsecret, require_one_of, utcnow
from baseframe import _

from . import db, BaseMixin, BaseScopedNameMixin
Expand Down Expand Up @@ -186,7 +186,7 @@ class ClientCredential(BaseMixin, db.Model):
#: OAuth client secret, hashed (64 chars hash plus 7 chars id prefix = 71 chars)
secret_hash = db.Column(db.String(71), nullable=False)
#: When was this credential last used for an API call?
accessed_at = db.Column(db.DateTime, nullable=True)
accessed_at = db.Column(db.TIMESTAMP(timezone=True), nullable=True)

def secret_is(self, candidate):
return self.secret_hash == 'sha256$' + sha256(candidate).hexdigest()
Expand Down Expand Up @@ -323,7 +323,7 @@ class AuthCode(ScopeMixin, BaseMixin, db.Model):
def is_valid(self):
# Time limit: 3 minutes. Should be reasonable enough to load a page
# on a slow mobile connection, without keeping the code valid too long
return not self.used and self.created_at >= datetime.utcnow() - timedelta(minutes=3)
return not self.used and self.created_at >= utcnow() - timedelta(minutes=3)


class AuthToken(ScopeMixin, BaseMixin, db.Model):
Expand Down Expand Up @@ -414,7 +414,7 @@ def algorithm(self, value):
def is_valid(self):
if self.validity == 0:
return True # This token is perpetually valid
now = datetime.utcnow()
now = utcnow()
if self.created_at < now - timedelta(seconds=self.validity):
return False
return True
Expand Down
2 changes: 1 addition & 1 deletion lastuser_core/models/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class SMSMessage(BaseMixin, db.Model):
message = db.Column(db.UnicodeText, nullable=False)
# Flags
status = db.Column(db.Integer, default=0, nullable=False)
status_at = db.Column(db.DateTime, nullable=True)
status_at = db.Column(db.TIMESTAMP(timezone=True), nullable=True)
fail_reason = db.Column(db.Unicode(25), nullable=True)


Expand Down
12 changes: 6 additions & 6 deletions lastuser_core/models/session.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-

from datetime import datetime, timedelta
from datetime import timedelta
from werkzeug import cached_property
from ua_parser import user_agent_parser
from flask import request
from coaster.utils import buid as make_buid
from coaster.utils import buid as make_buid, utcnow
from coaster.sqlalchemy import make_timestamp_columns
from . import db, BaseMixin
from .user import User
Expand Down Expand Up @@ -33,9 +33,9 @@ class UserSession(BaseMixin, db.Model):
ipaddr = db.Column(db.String(45), nullable=False)
user_agent = db.Column(db.Unicode(250), nullable=False)

accessed_at = db.Column(db.DateTime, nullable=False)
revoked_at = db.Column(db.DateTime, nullable=True)
sudo_enabled_at = db.Column(db.DateTime, nullable=False, default=db.func.utcnow())
accessed_at = db.Column(db.TIMESTAMP(timezone=True), nullable=False)
revoked_at = db.Column(db.TIMESTAMP(timezone=True), nullable=True)
sudo_enabled_at = db.Column(db.TIMESTAMP(timezone=True), nullable=False, default=db.func.utcnow())

def __init__(self, **kwargs):
super(UserSession, self).__init__(**kwargs)
Expand Down Expand Up @@ -72,7 +72,7 @@ def ua(self):

@property
def has_sudo(self):
return self.sudo_enabled_at > datetime.utcnow() - timedelta(hours=1)
return self.sudo_enabled_at > utcnow() - timedelta(hours=1)

def set_sudo(self):
self.sudo_enabled_at = db.func.utcnow()
Expand Down
12 changes: 6 additions & 6 deletions lastuser_core/models/user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-

from datetime import datetime, timedelta
from datetime import timedelta
from hashlib import md5
from werkzeug import check_password_hash, cached_property
import bcrypt
Expand All @@ -9,7 +9,7 @@
from sqlalchemy.orm import defer, deferred
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.associationproxy import association_proxy
from coaster.utils import newsecret, newpin, valid_username, require_one_of, LabeledEnum
from coaster.utils import newsecret, newpin, valid_username, require_one_of, LabeledEnum, utcnow
from coaster.sqlalchemy import make_timestamp_columns, failsafe_add, add_primary_relationship
from baseframe import _, __

Expand Down Expand Up @@ -160,9 +160,9 @@ class User(SharedNameMixin, UuidMixin, BaseMixin, db.Model):
#: Bcrypt hash of the user's password
pw_hash = db.Column(db.String(80), nullable=True)
#: Timestamp for when the user's password last changed
pw_set_at = db.Column(db.DateTime, nullable=True)
pw_set_at = db.Column(db.TIMESTAMP(timezone=True), nullable=True)
#: Expiry date for the password (to prompt user to reset it)
pw_expires_at = db.Column(db.DateTime, nullable=True)
pw_expires_at = db.Column(db.TIMESTAMP(timezone=True), nullable=True)
#: User's timezone
timezone = db.Column(db.Unicode(40), nullable=True)
#: User's status (active, suspended, merged, etc)
Expand Down Expand Up @@ -249,7 +249,7 @@ def _set_password(self, password):
password = property(fset=_set_password)

def password_has_expired(self):
return self.pw_hash is not None and self.pw_expires_at is not None and self.pw_expires_at <= datetime.utcnow()
return self.pw_hash is not None and self.pw_expires_at is not None and self.pw_expires_at <= utcnow()

def password_is(self, password):
if self.pw_hash is None:
Expand Down Expand Up @@ -1003,7 +1003,7 @@ class UserExternalId(BaseMixin, db.Model):
oauth_token_secret = db.Column(db.String(1000), nullable=True)
oauth_token_type = db.Column(db.String(250), nullable=True)

last_used_at = db.Column(db.DateTime, default=db.func.utcnow(), nullable=False)
last_used_at = db.Column(db.TIMESTAMP(timezone=True), default=db.func.utcnow(), nullable=False)

__table_args__ = (
db.UniqueConstraint('service', 'userid'),
Expand Down
7 changes: 4 additions & 3 deletions lastuser_oauth/views/helpers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-

from datetime import datetime, timedelta
from datetime import timedelta
from functools import wraps
from urllib import unquote
from pytz import common_timezones
import itsdangerous
from flask import current_app, request, session, flash, redirect, url_for, Response
from coaster.utils import utcnow
from coaster.auth import current_auth, add_auth_attribute, request_has_auth
from coaster.sqlalchemy import failsafe_add
from coaster.views import get_current_url
Expand Down Expand Up @@ -78,7 +79,7 @@ def lastuser_cookie(response):
Save lastuser login cookie and hasuser JS-readable flag cookie.
"""
if request_has_auth() and hasattr(current_auth, 'cookie'):
expires = datetime.utcnow() + timedelta(days=365)
expires = utcnow() + timedelta(days=365)
response.set_cookie('lastuser',
value=lastuser_oauth.serializer.dumps(current_auth.cookie, header_fields={'v': 1}),
max_age=31557600, # Keep this cookie for a year.
Expand Down Expand Up @@ -276,6 +277,6 @@ def register_internal(username, fullname, password):

def set_loginmethod_cookie(response, value):
response.set_cookie('login', value, max_age=31557600, # Keep this cookie for a year
expires=datetime.utcnow() + timedelta(days=365), # Expire one year from now
expires=utcnow() + timedelta(days=365), # Expire one year from now
httponly=True)
return response
6 changes: 3 additions & 3 deletions lastuser_oauth/views/login.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-

from __future__ import print_function
from datetime import datetime, timedelta
from datetime import timedelta
import urlparse
from openid import oidutil
from flask import current_app, redirect, request, flash, render_template, url_for, Markup, escape, abort
from flask_openid import OpenID
from coaster.utils import getbool
from coaster.utils import getbool, utcnow
from coaster.auth import current_auth
from coaster.views import get_next_url, load_model
from baseframe import _, __
Expand Down Expand Up @@ -231,7 +231,7 @@ def reset_email(user, kwargs):
if not resetreq:
return render_message(title=_("Invalid reset link"),
message=_(u"The reset link you clicked on is invalid"))
if resetreq.created_at < datetime.utcnow() - timedelta(days=1):
if resetreq.created_at < utcnow() - timedelta(days=1):
# Reset code has expired (> 24 hours). Delete it
db.session.delete(resetreq)
db.session.commit()
Expand Down
5 changes: 3 additions & 2 deletions manage.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python

from datetime import datetime, timedelta
from datetime import timedelta
from coaster.utils import utcnow
from coaster.manage import init_manager, Manager

import lastuser_core
Expand All @@ -19,7 +20,7 @@ def phoneclaims():
"""Sweep phone claims to close all unclaimed beyond expiry period (10m)"""
pc = models.UserPhoneClaim
pc.query.filter(
pc.updated_at < (datetime.utcnow() - timedelta(hours=1)),
pc.updated_at < (utcnow() - timedelta(hours=1)),
pc.verification_expired
).delete()
db.session.commit()
Expand Down
97 changes: 97 additions & 0 deletions migrations/versions/2b0f9d6ddf96_switch_to_timestamptz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Switch to timestamptz
Revision ID: 2b0f9d6ddf96
Revises: f324b0ecd05c
Create Date: 2019-05-10 01:22:55.904783
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '2b0f9d6ddf96'
down_revision = 'f324b0ecd05c'
branch_labels = None
depends_on = None

migrate_table_columns = [
('authcode', 'created_at'),
('authcode', 'updated_at'),
('authtoken', 'created_at'),
('authtoken', 'updated_at'),
('client', 'created_at'),
('client', 'updated_at'),
('client_credential', 'created_at'),
('client_credential', 'updated_at'),
('client_credential', 'accessed_at'),
('clientteamaccess', 'created_at'),
('clientteamaccess', 'updated_at'),
('name', 'created_at'),
('name', 'updated_at'),
('noticetype', 'created_at'),
('noticetype', 'updated_at'),
('organization', 'created_at'),
('organization', 'updated_at'),
('passwordresetrequest', 'created_at'),
('passwordresetrequest', 'updated_at'),
('permission', 'created_at'),
('permission', 'updated_at'),
('resource', 'created_at'),
('resource', 'updated_at'),
('resourceaction', 'created_at'),
('resourceaction', 'updated_at'),
('smsmessage', 'created_at'),
('smsmessage', 'updated_at'),
('smsmessage', 'status_at'),
('team', 'created_at'),
('team', 'updated_at'),
('teamclientpermissions', 'created_at'),
('teamclientpermissions', 'updated_at'),
('user', 'created_at'),
('user', 'updated_at'),
('user', 'pw_set_at'),
('user', 'pw_expires_at'),
('user_session', 'created_at'),
('user_session', 'updated_at'),
('user_session', 'accessed_at'),
('user_session', 'revoked_at'),
('user_session', 'sudo_enabled_at'),
('user_useremail_primary', 'created_at'),
('user_useremail_primary', 'updated_at'),
('user_userphone_primary', 'created_at'),
('user_userphone_primary', 'updated_at'),
('userclientpermissions', 'created_at'),
('userclientpermissions', 'updated_at'),
('useremail', 'created_at'),
('useremail', 'updated_at'),
('useremailclaim', 'created_at'),
('useremailclaim', 'updated_at'),
('userexternalid', 'created_at'),
('userexternalid', 'updated_at'),
('userexternalid', 'last_used_at'),
('userflashmessage', 'created_at'),
('userflashmessage', 'updated_at'),
('useroldid', 'created_at'),
('useroldid', 'updated_at'),
('userphone', 'created_at'),
('userphone', 'updated_at'),
('userphoneclaim', 'created_at'),
('userphoneclaim', 'updated_at'),
]


def upgrade():
for table, column in migrate_table_columns:
op.execute(sa.DDL(
'ALTER TABLE "%(table)s" ALTER COLUMN "%(column)s" TYPE TIMESTAMP WITH TIME ZONE USING "%(column)s" AT TIME ZONE \'UTC\'',
context={'table': table, 'column': column}
))


def downgrade():
for table, column in reversed(migrate_table_columns):
op.execute(sa.DDL(
'ALTER TABLE "%(table)s" ALTER COLUMN "%(column)s" TYPE TIMESTAMP WITHOUT TIME ZONE',
context={'table': table, 'column': column}
))
8 changes: 4 additions & 4 deletions tests/unit/lastuser_core/test_model_client_AuthToken.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from lastuserapp import db
import lastuser_core.models as models
from .test_db import TestDatabaseFixture
from datetime import datetime, timedelta
from coaster.utils import buid
from datetime import timedelta
from coaster.utils import buid, utcnow


class TestAuthToken(TestDatabaseFixture):
Expand Down Expand Up @@ -50,12 +50,12 @@ def test_authtoken_is_valid(self):

# scenario 3: when validity is limited
harry = models.User(username=u'harry', fullname=u'Harry Potter')
harry_token = models.AuthToken(client=client, user=harry, scope=scope, validity=3600, created_at=datetime.utcnow())
harry_token = models.AuthToken(client=client, user=harry, scope=scope, validity=3600, created_at=utcnow())
self.assertTrue(harry_token.is_valid())

# scenario 4: when validity is limited *and* the token has expired
cedric = models.User(username=u'cedric', fullname=u'Cedric Diggory')
cedric_token = models.AuthToken(client=client, user=cedric, scope=scope, validity=1, created_at=datetime.utcnow() - timedelta(1))
cedric_token = models.AuthToken(client=client, user=cedric, scope=scope, validity=1, created_at=utcnow() - timedelta(1))
self.assertFalse(cedric_token.is_valid())

def test_AuthToken_get(self):
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/lastuser_core/test_model_client_Client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-

from coaster.utils import utcnow
from lastuserapp import db
import lastuser_core.models as models
from .test_db import TestDatabaseFixture
from datetime import datetime


class TestClient(TestDatabaseFixture):
Expand Down Expand Up @@ -80,7 +80,7 @@ def test_client_authtoken_for(self):
# scenario 2: for a client that has confidential=False
varys = models.User(username=u'varys', fullname=u'Lord Varys')
house_lannisters = models.Client(title=u'House of Lannisters', confidential=False, user=varys, website=u'houseoflannisters.westeros')
varys_session = models.UserSession(user=varys, ipaddr='192.168.1.99', user_agent=u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36', accessed_at=datetime.utcnow())
varys_session = models.UserSession(user=varys, ipaddr='192.168.1.99', user_agent=u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36', accessed_at=utcnow())
lannisters_auth_token = models.AuthToken(client=house_lannisters, user=varys, scope=u'throne', validity=0, user_session=varys_session)
db.session.add_all([varys, house_lannisters, lannisters_auth_token, varys_session])
db.session.commit()
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/lastuser_core/test_model_user_User.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-

from datetime import timedelta
from sqlalchemy.orm.collections import InstrumentedList
from coaster.utils import utcnow

from lastuserapp import db
import lastuser_core.models as models
from sqlalchemy.orm.collections import InstrumentedList
from datetime import datetime, timedelta
from .test_db import TestDatabaseFixture


Expand Down Expand Up @@ -277,7 +279,7 @@ def test_user_password_has_expired(self):
"""
alexis = models.User(username=u'alexis', fullname=u'Alexis Castle')
alexis.password = u'unfortunateincidents'
alexis.pw_expires_at = datetime.utcnow() + timedelta(0, 0, 1)
alexis.pw_expires_at = utcnow() + timedelta(0, 0, 1)
db.session.add(alexis)
db.session.commit()
result = models.User.get(buid=alexis.buid)
Expand Down

0 comments on commit 5ea3423

Please sign in to comment.