from toto.exceptions import *
from toto.session import *
import toto.secret as secret
from time import time
import random
import string
from toto.tasks import InstancePool
[docs]class DBConnection(object):
'''Toto uses subclasses of DBConnection to support session and account storage as well as general
access to the backing database. Usually, direct access to the underlying database driver will
be available via the ``DBConnection.db`` property.
Currently, toto provides the following ``DBConnection`` drivers:
* ``toto.mongodbconnection.MongoDBConnection``
* ``toto.mysqldbconnection.MySQLdbConnection``
* ``toto.postgresconnection.PostgresConnection``
* ``toto.redisconnection.RedisConnection``
* ``toto.jsondbconnection.JSONConnection`` (For debugging only)
'''
_session_cache = None
def __init__(self, session_ttl=24*60*60*365, anon_session_ttl=24*60*60, session_renew=0, anon_session_renew=0, *args, **kwargs):
self.session_ttl = session_ttl
self.anon_session_ttl = anon_session_ttl or self.session_ttl
self.session_renew = session_renew or self.session_ttl
self.anon_session_renew = anon_session_renew or self.anon_session_ttl
[docs] def create_account(self, user_id, password, additional_values={}, **values):
'''Create an account for the given ``user_id`` and ``password``. Optionally set additional account
values by passing them as keyword arguments (the ``additional_values`` parameter is deprecated).
Note: if your database uses a predefined schema, make sure to create the appropriate columns
before passing additional arguments to ``create_account``.
'''
if not user_id:
raise TotoException(ERROR_INVALID_USER_ID, "Invalid user ID.")
user_id = user_id.lower()
account = self._get_account(user_id)
if account:
raise TotoException(ERROR_USER_ID_EXISTS, "User ID already in use.")
values.update(additional_values)
values['user_id'] = user_id
values['password'] = secret.password_hash(password)
self._store_account(user_id, values)
[docs] def create_session(self, user_id=None, password=None, verify_password=True, key=None):
'''Create a new session for the account with the given ``user_id`` and ``password``, or an anonymous
session if anonymous sessions are enabled. This method returns a subclass of ``TotoSession``
designed for the current backing database. Pass ``verify_password=False`` to create a session
without checking the password. This feature can be used to implement alternative authentication
methods like Facebook, Twitter or Google+.
'''
if not user_id:
user_id = ''
user_id = user_id.lower()
account = user_id and self._get_account(user_id)
if user_id and (not account or (verify_password and not secret.verify_password(password, account['password']))):
raise TotoException(ERROR_USER_NOT_FOUND, "Invalid user ID or password")
session_id = TotoSession.generate_id()
expires = time() + (user_id and self.session_ttl or self.anon_session_ttl)
session_data = {'user_id': user_id, 'expires': expires, 'session_id': session_id}
if key:
session_data['key'] = key
self._prepare_session(account, session_data)
if not self._cache_session_data(session_data):
self._store_session(session_id, session_data)
session = self._instantiate_session(session_data, self._session_cache)
return session
[docs] def retrieve_session(self, session_id):
'''Retrieve an existing session with the given ``session_id``. This method returns a
subclass of ``TotoSession`` designed for the current backing database.
The use of HTTPS is strongly recommended for any communication involving sensitive information.
'''
session_data = self._load_session_data(session_id)
if not session_data:
return None
user_id = session_data['user_id']
expires = time() + (user_id and self.session_renew or self.anon_session_renew)
if session_data['expires'] < expires:
session_data['expires'] = expires
if not self._cache_session_data(session_data):
self._update_expiry(session_id, session_data)
session = self._instantiate_session(session_data, self._session_cache)
return session
[docs] def change_password(self, user_id, password, new_password):
'''Updates the password for the account with the given ``user_id`` and ``password`` to match
``new_password`` for all future requests.
'''
user_id = user_id.lower()
account = self._get_account(user_id)
if not account or not secret.verify_password(password, account['password']):
raise TotoException(ERROR_USER_NOT_FOUND, "Invalid user ID or password")
self._update_password(user_id, account, secret.password_hash(new_password))
[docs] def generate_password(self, user_id):
'''Generates a new password for the account with the given ``user_id`` and makes it active
for all future requests. The new password will be returned. This method is designed to
support "forgot password" functionality.
'''
user_id = user_id.lower()
account = self._get_account(user_id)
if not account:
raise TotoException(ERROR_USER_NOT_FOUND, "Invalid user ID")
pass_chars = string.ascii_letters + string.digits
new_password = ''.join([random.choice(pass_chars) for x in xrange(10)])
self._update_password(user_id, account, secret.password_hash(new_password))
return new_password
[docs] def remove_session(self, session_id):
'''Invalidate the session with the given ``session_id``.
'''
if self._session_cache:
self._session_cache.remove_session(session_id)
else:
self._remove_session(session_id)
[docs] def _remove_session(self, session_id):
'''Called by ``DBConnection.remove_session`` to invalidate the specified session when no session cache is in use.
'''
raise NotImplementedError()
[docs] def clear_sessions(self, user_id):
'''If implemented, invalidates all sessions tied to the account with the given ``user_id``.
'''
pass
[docs] def set_session_cache(self, session_cache):
'''Optionally set an instance of ``TotoSessionCache`` that will be used to store sessions separately from
this database.
'''
self._session_cache = session_cache
def _load_session_data(self, session_id):
'''Called by ``DBConnection.retrieve_session``. Will attempt to load data from an associated ``TotoSessionCache``.
If no ``TotoSessionCache`` is associated with the current instance, the result of ``self._load_uncached_data(session_id)``
is returned.
'''
if self._session_cache:
return self._session_cache.load_session(session_id)
return self._load_uncached_data(session_id)
[docs] def _load_uncached_data(self, session_id):
'''Load a session data ``dict`` from the local database. Called by default and if no ``TotoSessionCache`` has been
associated with the current instance of ``DBConnection``.
'''
raise NotImplementedError()
def _cache_session_data(self, session_data):
'''Called by ``DBConnection.create_session`` and by ``DBConnection.retrieve_session`` if there is a change in ``TotoSession.expires``.
Returns ``True`` if the session has been written to an associated ``TotoSessionCache``, ``False`` otherwise.
'''
if self._session_cache:
updated_session_id = self._session_cache.store_session(session_data)
if updated_session_id:
session_data['session_id'] = updated_session_id
return True
return False
[docs] def _store_session(self, session_id, session_data):
'''Called by ``DBConnection.create_session``, and by ``DBConnection.retrieve_session`` if there is a change in ``TotoSession.expires``.
Will not be called if a session cache has been attached to the ``DBConnection``.
'''
raise NotImplementedError()
[docs] def _update_expiry(self, session_id, session_data):
'''Called by ``DBConnection.retrieve_session`` if there is a change in ``TotoSession.expires`` to allow optional efficient updates.
If not implemented, ``self._store_session`` will be called.
'''
return self._store_session(session_id, session_data)
[docs] def _update_password(self, user_id, hashed_password):
'''Called by ``DBConnection.change_password`` and ``DBConnection.generate_password``.
'''
raise NotImplementedError()
[docs] def _instantiate_session(self, session_data, session_cache):
'''Called by ``DBConnection.create_session`` and by ``DBConnection.retrieve_session`` to actually instantiate a ``TotoSession``
instance. Must return a new ``TotoSession``.
'''
raise NotImplementedError()
[docs] def _get_account(self, user_id):
'''Called by ``DBConnection.create_session`` if ``verify_password=True`` and must return a dictionry containing
at least the pair ``'password':<hashed_password``.
'''
raise NotImplementedError()
[docs] def _prepare_session(self, account, session_data):
'''Called by ``DBConnection.create_session`` before the session is written to the database to allow for the
addition of any extra data that may be needed by the ``subclass.TotoSession`` implementation.
'''
pass
[docs] def _store_account(self, user_id, values):
'''Must be implemented in subclasses to persist new accounts to the database. Values is a dictionary
that will contain, at a minimum, ``user_id`` and ``password``. ``password`` will be the hashed password
passed to ``self.create_account()``.
'''
raise NotImplementedError()
from tornado.options import define, options
define("database", metavar='mysql|mongodb|redis|postgres|json|none', default="none", help="the database driver to use")
define("db_host", default='localhost', help="The host to use for database connections.")
define("db_port", default=0, help="The port to use for database connections. Leave this at zero to use the default for the selected database type")
define("mysql_database", type=str, help="Main MySQL schema name")
define("mysql_user", type=str, help="Main MySQL user")
define("mysql_password", type=str, help="Main MySQL user password")
define("mysql_uuid_account_id", default=False, help="Use binary(16) UUIDs for account_id in MySQL databases instead of int(8) unsigned auto_increment. UUID(bytes=handler.session.account_id) can be used to get a UUID representing the account_id.")
define("postgres_database", type=str, help="Main Postgres database name")
define("postgres_user", type=str, help="Main Postgres user")
define("postgres_password", type=str, help="Main Postgres user password")
define("postgres_min_connections", type=int, default=1, help="The minimum number of connections to keep in the Postgres connection pool")
define("postgres_max_connections", type=int, default=100, help="The maximum number of connections to keep in the Postgres connection pool")
define("mongodb_database", default="toto_server", help="MongoDB database")
define("redis_database", default=0, help="Redis DB")
define("session_ttl", default=24*60*60*365, help="The number of seconds after creation a session should expire")
define("anon_session_ttl", default=24*60*60, help="The number of seconds after creation an anonymous session should expire")
define("session_renew", default=0, help="The number of seconds before a session expires that it should be renewed, or zero to renew on every request")
define("anon_session_renew", default=0, help="The number of seconds before an anonymous session expires that it should be renewed, or zero to renew on every request")
def configured_connection():
'''Returns a new database connection based on the configuration options
'''
if options.database == "mongodb":
from mongodbconnection import MongoDBConnection
return MongoDBConnection(options.db_host, options.db_port or 27017, options.mongodb_database, session_ttl=options.session_ttl, anon_session_ttl=options.anon_session_ttl, session_renew=options.session_renew, anon_session_renew=options.anon_session_renew)
elif options.database == "redis":
from redisconnection import RedisConnection
return RedisConnection(options.db_host, options.db_port or 6379, options.redis_database, session_ttl=options.session_ttl, anon_session_ttl=options.anon_session_ttl, session_renew=options.session_renew, anon_session_renew=options.anon_session_renew)
elif options.database == "mysql":
from mysqldbconnection import MySQLdbConnection
return MySQLdbConnection('%s:%s' % (options.db_host, options.db_port or 3306), options.mysql_database, options.mysql_user, options.mysql_password, session_ttl=options.session_ttl, anon_session_ttl=options.anon_session_ttl, session_renew=options.session_renew, anon_session_renew=options.anon_session_renew, uuid_account_id=options.mysql_uuid_account_id)
elif options.database == 'postgres':
from postgresconnection import PostgresConnection
return PostgresConnection(options.db_host, options.db_port or 5432, options.postgres_database, options.postgres_user, options.postgres_password, session_ttl=options.session_ttl, anon_session_ttl=options.anon_session_ttl, session_renew=options.session_renew, anon_session_renew=options.anon_session_renew, min_connections=options.postgres_min_connections, max_connections=options.postgres_max_connections)
elif options.database == 'json':
from jsondbconnection import JSONConnection
# return JSONConnection(options.db_host, options.db_port, session_ttl=options.session_ttl, anon_session_ttl=options.anon_session_ttl, session_renew=options.session_renew, anon_session_renew=options.anon_session_renew)
return InstancePool(JSONConnection(options.db_host, options.db_port, session_ttl=options.session_ttl, anon_session_ttl=options.anon_session_ttl, session_renew=options.session_renew, anon_session_renew=options.anon_session_renew))
else:
from fakeconnection import FakeConnection
return FakeConnection()