123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 |
- """
- This module contains C{L{OpenIDStore}} implementations that use
- various SQL databases to back them.
- Example of how to initialize a store database::
- python -c 'from openid.store import sqlstore; import pysqlite2.dbapi2; sqlstore.SQLiteStore(pysqlite2.dbapi2.connect("cstore.db")).createTables()'
- """
- import re
- import time
- from openid.association import Association
- from openid.store.interface import OpenIDStore
- from openid.store import nonce
- def _inTxn(func):
- def wrapped(self, *args, **kwargs):
- return self._callInTransaction(func, self, *args, **kwargs)
- if hasattr(func, '__name__'):
- try:
- wrapped.__name__ = func.__name__[4:]
- except TypeError:
- pass
- if hasattr(func, '__doc__'):
- wrapped.__doc__ = func.__doc__
- return wrapped
- class SQLStore(OpenIDStore):
- """
- This is the parent class for the SQL stores, which contains the
- logic common to all of the SQL stores.
- The table names used are determined by the class variables
- C{L{associations_table}} and
- C{L{nonces_table}}. To change the name of the tables used, pass
- new table names into the constructor.
- To create the tables with the proper schema, see the
- C{L{createTables}} method.
- This class shouldn't be used directly. Use one of its subclasses
- instead, as those contain the code necessary to use a specific
- database.
- All methods other than C{L{__init__}} and C{L{createTables}}
- should be considered implementation details.
- @cvar associations_table: This is the default name of the table to
- keep associations in
- @cvar nonces_table: This is the default name of the table to keep
- nonces in.
- @sort: __init__, createTables
- """
- associations_table = 'oid_associations'
- nonces_table = 'oid_nonces'
- def __init__(self, conn, associations_table=None, nonces_table=None):
- """
- This creates a new SQLStore instance. It requires an
- established database connection be given to it, and it allows
- overriding the default table names.
- @param conn: This must be an established connection to a
- database of the correct type for the SQLStore subclass
- you're using.
- @type conn: A python database API compatible connection
- object.
- @param associations_table: This is an optional parameter to
- specify the name of the table used for storing
- associations. The default value is specified in
- C{L{SQLStore.associations_table}}.
- @type associations_table: C{str}
- @param nonces_table: This is an optional parameter to specify
- the name of the table used for storing nonces. The
- default value is specified in C{L{SQLStore.nonces_table}}.
- @type nonces_table: C{str}
- """
- self.conn = conn
- self.cur = None
- self._statement_cache = {}
- self._table_names = {
- 'associations': associations_table or self.associations_table,
- 'nonces': nonces_table or self.nonces_table,
- }
- self.max_nonce_age = 6 * 60 * 60 # Six hours, in seconds
- # DB API extension: search for "Connection Attributes .Error,
- # .ProgrammingError, etc." in
- # http://www.python.org/dev/peps/pep-0249/
- if (hasattr(self.conn, 'IntegrityError') and
- hasattr(self.conn, 'OperationalError')):
- self.exceptions = self.conn
- if not (hasattr(self.exceptions, 'IntegrityError') and
- hasattr(self.exceptions, 'OperationalError')):
- raise RuntimeError("Error using database connection module "
- "(Maybe it can't be imported?)")
- def blobDecode(self, blob):
- """Convert a blob as returned by the SQL engine into a str object.
- str -> str"""
- return blob
- def blobEncode(self, s):
- """Convert a str object into the necessary object for storing
- in the database as a blob."""
- return s
- def _getSQL(self, sql_name):
- try:
- return self._statement_cache[sql_name]
- except KeyError:
- sql = getattr(self, sql_name)
- sql %= self._table_names
- self._statement_cache[sql_name] = sql
- return sql
- def _execSQL(self, sql_name, *args):
- sql = self._getSQL(sql_name)
- # Kludge because we have reports of postgresql not quoting
- # arguments if they are passed in as unicode instead of str.
- # Currently the strings in our tables just have ascii in them,
- # so this ought to be safe.
- def unicode_to_str(arg):
- if isinstance(arg, unicode):
- return str(arg)
- else:
- return arg
- str_args = map(unicode_to_str, args)
- self.cur.execute(sql, str_args)
- def __getattr__(self, attr):
- # if the attribute starts with db_, use a default
- # implementation that looks up the appropriate SQL statement
- # as an attribute of this object and executes it.
- if attr[:3] == 'db_':
- sql_name = attr[3:] + '_sql'
- def func(*args):
- return self._execSQL(sql_name, *args)
- setattr(self, attr, func)
- return func
- else:
- raise AttributeError('Attribute %r not found' % (attr,))
- def _callInTransaction(self, func, *args, **kwargs):
- """Execute the given function inside of a transaction, with an
- open cursor. If no exception is raised, the transaction is
- comitted, otherwise it is rolled back."""
- # No nesting of transactions
- self.conn.rollback()
- try:
- self.cur = self.conn.cursor()
- try:
- ret = func(*args, **kwargs)
- finally:
- self.cur.close()
- self.cur = None
- except:
- self.conn.rollback()
- raise
- else:
- self.conn.commit()
- return ret
- def txn_createTables(self):
- """
- This method creates the database tables necessary for this
- store to work. It should not be called if the tables already
- exist.
- """
- self.db_create_nonce()
- self.db_create_assoc()
- createTables = _inTxn(txn_createTables)
- def txn_storeAssociation(self, server_url, association):
- """Set the association for the server URL.
- Association -> NoneType
- """
- a = association
- self.db_set_assoc(
- server_url,
- a.handle,
- self.blobEncode(a.secret),
- a.issued,
- a.lifetime,
- a.assoc_type)
- storeAssociation = _inTxn(txn_storeAssociation)
- def txn_getAssociation(self, server_url, handle=None):
- """Get the most recent association that has been set for this
- server URL and handle.
- str -> NoneType or Association
- """
- if handle is not None:
- self.db_get_assoc(server_url, handle)
- else:
- self.db_get_assocs(server_url)
- rows = self.cur.fetchall()
- if len(rows) == 0:
- return None
- else:
- associations = []
- for values in rows:
- assoc = Association(*values)
- assoc.secret = self.blobDecode(assoc.secret)
- if assoc.getExpiresIn() == 0:
- self.txn_removeAssociation(server_url, assoc.handle)
- else:
- associations.append((assoc.issued, assoc))
- if associations:
- associations.sort()
- return associations[-1][1]
- else:
- return None
- getAssociation = _inTxn(txn_getAssociation)
- def txn_removeAssociation(self, server_url, handle):
- """Remove the association for the given server URL and handle,
- returning whether the association existed at all.
- (str, str) -> bool
- """
- self.db_remove_assoc(server_url, handle)
- return self.cur.rowcount > 0 # -1 is undefined
- removeAssociation = _inTxn(txn_removeAssociation)
- def txn_useNonce(self, server_url, timestamp, salt):
- """Return whether this nonce is present, and if it is, then
- remove it from the set.
- str -> bool"""
- if abs(timestamp - time.time()) > nonce.SKEW:
- return False
- try:
- self.db_add_nonce(server_url, timestamp, salt)
- except self.exceptions.IntegrityError:
- # The key uniqueness check failed
- return False
- else:
- # The nonce was successfully added
- return True
- useNonce = _inTxn(txn_useNonce)
- def txn_cleanupNonces(self):
- self.db_clean_nonce(int(time.time()) - nonce.SKEW)
- return self.cur.rowcount
- cleanupNonces = _inTxn(txn_cleanupNonces)
- def txn_cleanupAssociations(self):
- self.db_clean_assoc(int(time.time()))
- return self.cur.rowcount
- cleanupAssociations = _inTxn(txn_cleanupAssociations)
- class SQLiteStore(SQLStore):
- """
- This is an SQLite-based specialization of C{L{SQLStore}}.
- To create an instance, see C{L{SQLStore.__init__}}. To create the
- tables it will use, see C{L{SQLStore.createTables}}.
- All other methods are implementation details.
- """
- create_nonce_sql = """
- CREATE TABLE %(nonces)s (
- server_url VARCHAR,
- timestamp INTEGER,
- salt CHAR(40),
- UNIQUE(server_url, timestamp, salt)
- );
- """
- create_assoc_sql = """
- CREATE TABLE %(associations)s
- (
- server_url VARCHAR(2047),
- handle VARCHAR(255),
- secret BLOB(128),
- issued INTEGER,
- lifetime INTEGER,
- assoc_type VARCHAR(64),
- PRIMARY KEY (server_url, handle)
- );
- """
- set_assoc_sql = ('INSERT OR REPLACE INTO %(associations)s '
- '(server_url, handle, secret, issued, '
- 'lifetime, assoc_type) '
- 'VALUES (?, ?, ?, ?, ?, ?);')
- get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type '
- 'FROM %(associations)s WHERE server_url = ?;')
- get_assoc_sql = (
- 'SELECT handle, secret, issued, lifetime, assoc_type '
- 'FROM %(associations)s WHERE server_url = ? AND handle = ?;')
- get_expired_sql = ('SELECT server_url '
- 'FROM %(associations)s WHERE issued + lifetime < ?;')
- remove_assoc_sql = ('DELETE FROM %(associations)s '
- 'WHERE server_url = ? AND handle = ?;')
- clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < ?;'
- add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (?, ?, ?);'
- clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < ?;'
- def blobDecode(self, buf):
- return str(buf)
- def blobEncode(self, s):
- return buffer(s)
- def useNonce(self, *args, **kwargs):
- # Older versions of the sqlite wrapper do not raise
- # IntegrityError as they should, so we have to detect the
- # message from the OperationalError.
- try:
- return super(SQLiteStore, self).useNonce(*args, **kwargs)
- except self.exceptions.OperationalError, why:
- if re.match('^columns .* are not unique$', why[0]):
- return False
- else:
- raise
- class MySQLStore(SQLStore):
- """
- This is a MySQL-based specialization of C{L{SQLStore}}.
- Uses InnoDB tables for transaction support.
- To create an instance, see C{L{SQLStore.__init__}}. To create the
- tables it will use, see C{L{SQLStore.createTables}}.
- All other methods are implementation details.
- """
- try:
- import MySQLdb as exceptions
- except ImportError:
- exceptions = None
- create_nonce_sql = """
- CREATE TABLE %(nonces)s (
- server_url BLOB NOT NULL,
- timestamp INTEGER NOT NULL,
- salt CHAR(40) NOT NULL,
- PRIMARY KEY (server_url(255), timestamp, salt)
- )
- ENGINE=InnoDB;
- """
- create_assoc_sql = """
- CREATE TABLE %(associations)s
- (
- server_url BLOB NOT NULL,
- handle VARCHAR(255) NOT NULL,
- secret BLOB NOT NULL,
- issued INTEGER NOT NULL,
- lifetime INTEGER NOT NULL,
- assoc_type VARCHAR(64) NOT NULL,
- PRIMARY KEY (server_url(255), handle)
- )
- ENGINE=InnoDB;
- """
- set_assoc_sql = ('REPLACE INTO %(associations)s '
- 'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);')
- get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type'
- ' FROM %(associations)s WHERE server_url = %%s;')
- get_expired_sql = ('SELECT server_url '
- 'FROM %(associations)s WHERE issued + lifetime < %%s;')
- get_assoc_sql = (
- 'SELECT handle, secret, issued, lifetime, assoc_type'
- ' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;')
- remove_assoc_sql = ('DELETE FROM %(associations)s '
- 'WHERE server_url = %%s AND handle = %%s;')
- clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;'
- add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);'
- clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
- def blobDecode(self, blob):
- if type(blob) is str:
- # Versions of MySQLdb >= 1.2.2
- return blob
- else:
- # Versions of MySQLdb prior to 1.2.2 (as far as we can tell)
- return blob.tostring()
- class PostgreSQLStore(SQLStore):
- """
- This is a PostgreSQL-based specialization of C{L{SQLStore}}.
- To create an instance, see C{L{SQLStore.__init__}}. To create the
- tables it will use, see C{L{SQLStore.createTables}}.
- All other methods are implementation details.
- """
- try:
- import psycopg as exceptions
- except ImportError:
- # psycopg2 has the dbapi extension where the exception classes
- # are available on the connection object. A psycopg2
- # connection will use the correct exception classes because of
- # this, and a psycopg connection will fall through to use the
- # psycopg imported above.
- exceptions = None
- create_nonce_sql = """
- CREATE TABLE %(nonces)s (
- server_url VARCHAR(2047) NOT NULL,
- timestamp INTEGER NOT NULL,
- salt CHAR(40) NOT NULL,
- PRIMARY KEY (server_url, timestamp, salt)
- );
- """
- create_assoc_sql = """
- CREATE TABLE %(associations)s
- (
- server_url VARCHAR(2047) NOT NULL,
- handle VARCHAR(255) NOT NULL,
- secret BYTEA NOT NULL,
- issued INTEGER NOT NULL,
- lifetime INTEGER NOT NULL,
- assoc_type VARCHAR(64) NOT NULL,
- PRIMARY KEY (server_url, handle),
- CONSTRAINT secret_length_constraint CHECK (LENGTH(secret) <= 128)
- );
- """
- def db_set_assoc(self, server_url, handle, secret, issued, lifetime, assoc_type):
- """
- Set an association. This is implemented as a method because
- REPLACE INTO is not supported by PostgreSQL (and is not
- standard SQL).
- """
- result = self.db_get_assoc(server_url, handle)
- rows = self.cur.fetchall()
- if len(rows):
- # Update the table since this associations already exists.
- return self.db_update_assoc(secret, issued, lifetime, assoc_type,
- server_url, handle)
- else:
- # Insert a new record because this association wasn't
- # found.
- return self.db_new_assoc(server_url, handle, secret, issued,
- lifetime, assoc_type)
- new_assoc_sql = ('INSERT INTO %(associations)s '
- 'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);')
- update_assoc_sql = ('UPDATE %(associations)s SET '
- 'secret = %%s, issued = %%s, '
- 'lifetime = %%s, assoc_type = %%s '
- 'WHERE server_url = %%s AND handle = %%s;')
- get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type'
- ' FROM %(associations)s WHERE server_url = %%s;')
- get_expired_sql = ('SELECT server_url '
- 'FROM %(associations)s WHERE issued + lifetime < %%s;')
- get_assoc_sql = (
- 'SELECT handle, secret, issued, lifetime, assoc_type'
- ' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;')
- remove_assoc_sql = ('DELETE FROM %(associations)s '
- 'WHERE server_url = %%s AND handle = %%s;')
- clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;'
- add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);'
- clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
- def blobEncode(self, blob):
- try:
- from psycopg2 import Binary
- except ImportError:
- from psycopg import Binary
- return Binary(blob)
|