"""
Contains all model classes relevant to management of users
"""
from datetime import datetime, timedelta
from hashlib import sha256
from uuid import uuid1, UUID
from database.models import Base
from passlib.apps import custom_app_context as pwd_context
from sqlalchemy import desc
from sqlalchemy.orm import relationship
from config import default_config as conf
from database import schema
from database.models.projects import Project
from database.sessions import ContextManagedSession
__author__ = 'Michal Kononenko'
[docs]class Token(Base):
"""
Contains methods for manipulating user tokens.
"""
__table__ = schema.tokens
token_hash = __table__.c.token_hash
date_created = __table__.c.date_created
expiration_date = __table__.c.expiration_date
def __init__(
self, token_string, expiration_date=None, owner=None
):
if isinstance(token_string, UUID):
token_string = str(token_string)
self.date_created = datetime.utcnow()
self.token_hash = self.hash_token(token_string)
if expiration_date is None:
expiration_date = self.date_created + timedelta(
seconds=conf.DEFAULT_TOKEN_EXPIRATION_TIME)
self.expiration_date = expiration_date
self.owner = owner
@staticmethod
[docs] def hash_token(token_string):
"""
Takes a token string and hashes it using SHA256.
"""
return sha256(token_string.encode('ascii')).hexdigest()
[docs] def verify_token(self, token):
"""
Checks if the provided token string matches the token hash in the DB,
and that the token is not expired
:param str token: The token to verify
:return: True if the token is valid, False if not
"""
if datetime.utcnow() > self.expiration_date:
return False
return self.hash_token(token) == self.token_hash
[docs] def revoke(self):
"""
Expire the token by setting the expiration date equal to the current
date
"""
self.expiration_date = datetime.utcnow()
[docs]class User(Base):
"""
Base class for a User.
"""
__table__ = schema.users
__columns__ = __table__.c
id = __columns__.user_id
username = __columns__.username
email_address = __columns__.email_address
password_hash = __columns__.password_hash
date_created = __columns__.date_created
__mapper_args__ = {
'polymorphic_identity': False,
'polymorphic_on': __table__.c.is_superuser
}
projects = relationship(Project, backref='members',
secondary=schema.users_projects_asoc_tables,
lazy='dynamic')
owned_projects = relationship(Project, backref='owner',
foreign_keys=schema.projects.c.owner_id)
tokens = relationship(Token, backref='owner', lazy='dynamic')
def __init__(
self, username, password, email,
date_created=datetime.now()
):
self.password_hash = self.hash_password(password)
self.username = username
self.email_address = email
self.date_created = date_created
@classmethod
[docs] def from_session(cls, username, session):
"""
Provides an alternate "constructor" by using the supplied
session and returning the user matching the given username.
The method also asserts that a user was returned by the query.
If it did not happen, something horrible has happened.
:param str username: The username of the user to get
:param ContextManagedSession session: The session to use for retrieving
the user
:return: The user to be retrieved
"""
user = session.query(cls).filter_by(username=username).first()
if not isinstance(user, cls):
raise TypeError(
'The returned user of type %s is not of expected type %s',
user.__class__.__name__, cls.__name__
)
return user
@staticmethod
[docs] def hash_password(password):
"""
Hash the user's password
:param str password: The password to hash
:return:
"""
return pwd_context.encrypt(password)
[docs] def verify_password(self, password):
"""
Verify the user's password
:param str password: The password to verify
:return: True if the password is correct, else False
:rtype: bool
"""
return pwd_context.verify(password, self.password_hash)
[docs] def generate_auth_token(
self, expiration=conf.DEFAULT_TOKEN_EXPIRATION_TIME,
session=ContextManagedSession(bind=conf.DATABASE_ENGINE)
):
"""
Generates a token for the user. The user's token is a `UUID 1`_,
randomly generated in this function. A :class:`Token` is created
with this randomly-generated UUID, the UUID token is hashed,
and stored in the database.
.. warning::
The string representation of the generated token is returned
only once, and is not recoverable from the data stored in the
database. The returned token is also a proxy for the user's
password, so treat it as such.
:param int expiration: The lifetime of the token in seconds.
:param ContextManagedSession session: The database session with
which this method will interact, in order to produce a token. By
default, this is a :class:`ContextManagedSession` that will
point to :attr:`conf.DATABASE_ENGINE`, but for the purposes of unit
testing, it can be repointed.
:return: A tuple containing the newly-created authentication token,
and the expiration date of the new token. The expiration date
is an object of type :class:`Datetime`
:rtype: tuple(str, Datetime)
.. _UUID 1: https://goo.gl/iUS6s9
"""
expiration_date = datetime.utcnow() + timedelta(seconds=expiration)
token_string = str(uuid1())
with session() as session:
user = session.query(self.__class__).filter_by(
id=self.id
).first()
token = Token(token_string, expiration_date, owner=user)
session.add(token)
return token_string, expiration_date
[docs] def verify_auth_token(self, token_string):
"""
Loads the user's current token, and uses that token to check whether
the supplied token string is correct
:param str token_string: The token to validate
:return: ``True`` if the token is valid and ``False`` if not
"""
token = self.current_token
return token.verify_token(token_string)
@property
def current_token(self):
return self.tokens.order_by(desc(Token.date_created))
@property
def get(self):
return {'username': self.username, 'email': self.email_address,
'date_created': self.date_created.isoformat()}
@property
def get_full(self):
return {
'username': self.username,
'email': self.email_address,
'date_created': self.date_created.isoformat(),
'projects': [project.get_full for project in self.projects.all()]
}
def __repr__(self):
return '%s(%s, %s, %s)' % (
self.__class__.__name__, self.username,
self.password_hash, self.email_address
)
[docs] def __eq__(self, other):
"""
:param User other: The user against which to compare
"""
return self.username == other.username
[docs] def __ne__(self, other):
"""
Check if two users are not equal
:param other:
"""
return self.username != other.username
[docs]class Administrator(User):
"""
Represents a "superuser" type. The administrator will be able to oversee
all projects, revoke anyone's token, and approve new users into the system,
when user approval is complete.
"""
__mapper_args__ = {
'polymorphic_identity': True
}