2 This module contains C{L{OpenIDStore}} implementations that use
3 various SQL databases to back them.
5 Example of how to initialize a store database::
7 python -c 'from openid.store import sqlstore; import pysqlite2.dbapi2; sqlstore.SQLiteStore(pysqlite2.dbapi2.connect("cstore.db")).createTables()'
12 from openid
.association
import Association
13 from openid
.store
.interface
import OpenIDStore
14 from openid
.store
import nonce
17 def wrapped(self
, *args
, **kwargs
):
18 return self
._callInTransaction
(func
, self
, *args
, **kwargs
)
20 if hasattr(func
, '__name__'):
22 wrapped
.__name
__ = func
.__name
__[4:]
26 if hasattr(func
, '__doc__'):
27 wrapped
.__doc
__ = func
.__doc
__
31 class SQLStore(OpenIDStore
):
33 This is the parent class for the SQL stores, which contains the
34 logic common to all of the SQL stores.
36 The table names used are determined by the class variables
37 C{L{associations_table}} and
38 C{L{nonces_table}}. To change the name of the tables used, pass
39 new table names into the constructor.
41 To create the tables with the proper schema, see the
42 C{L{createTables}} method.
44 This class shouldn't be used directly. Use one of its subclasses
45 instead, as those contain the code necessary to use a specific
48 All methods other than C{L{__init__}} and C{L{createTables}}
49 should be considered implementation details.
52 @cvar associations_table: This is the default name of the table to
55 @cvar nonces_table: This is the default name of the table to keep
59 @sort: __init__, createTables
62 associations_table
= 'oid_associations'
63 nonces_table
= 'oid_nonces'
65 def __init__(self
, conn
, associations_table
=None, nonces_table
=None):
67 This creates a new SQLStore instance. It requires an
68 established database connection be given to it, and it allows
69 overriding the default table names.
72 @param conn: This must be an established connection to a
73 database of the correct type for the SQLStore subclass
76 @type conn: A python database API compatible connection
80 @param associations_table: This is an optional parameter to
81 specify the name of the table used for storing
82 associations. The default value is specified in
83 C{L{SQLStore.associations_table}}.
85 @type associations_table: C{str}
88 @param nonces_table: This is an optional parameter to specify
89 the name of the table used for storing nonces. The
90 default value is specified in C{L{SQLStore.nonces_table}}.
92 @type nonces_table: C{str}
96 self
._statement
_cache
= {}
98 'associations': associations_table
or self
.associations_table
,
99 'nonces': nonces_table
or self
.nonces_table
,
101 self
.max_nonce_age
= 6 * 60 * 60 # Six hours, in seconds
103 # DB API extension: search for "Connection Attributes .Error,
104 # .ProgrammingError, etc." in
105 # http://www.python.org/dev/peps/pep-0249/
106 if (hasattr(self
.conn
, 'IntegrityError') and
107 hasattr(self
.conn
, 'OperationalError')):
108 self
.exceptions
= self
.conn
110 if not (hasattr(self
.exceptions
, 'IntegrityError') and
111 hasattr(self
.exceptions
, 'OperationalError')):
112 raise RuntimeError("Error using database connection module "
113 "(Maybe it can't be imported?)")
115 def blobDecode(self
, blob
):
116 """Convert a blob as returned by the SQL engine into a str object.
121 def blobEncode(self
, s
):
122 """Convert a str object into the necessary object for storing
123 in the database as a blob."""
126 def _getSQL(self
, sql_name
):
128 return self
._statement
_cache
[sql_name
]
130 sql
= getattr(self
, sql_name
)
131 sql
%= self
._table
_names
132 self
._statement
_cache
[sql_name
] = sql
135 def _execSQL(self
, sql_name
, *args
):
136 sql
= self
._getSQL
(sql_name
)
137 # Kludge because we have reports of postgresql not quoting
138 # arguments if they are passed in as unicode instead of str.
139 # Currently the strings in our tables just have ascii in them,
140 # so this ought to be safe.
141 def unicode_to_str(arg
):
142 if isinstance(arg
, unicode):
146 str_args
= map(unicode_to_str
, args
)
147 self
.cur
.execute(sql
, str_args
)
149 def __getattr__(self
, attr
):
150 # if the attribute starts with db_, use a default
151 # implementation that looks up the appropriate SQL statement
152 # as an attribute of this object and executes it.
153 if attr
[:3] == 'db_':
154 sql_name
= attr
[3:] + '_sql'
156 return self
._execSQL
(sql_name
, *args
)
157 setattr(self
, attr
, func
)
160 raise AttributeError('Attribute %r not found' % (attr
,))
162 def _callInTransaction(self
, func
, *args
, **kwargs
):
163 """Execute the given function inside of a transaction, with an
164 open cursor. If no exception is raised, the transaction is
165 comitted, otherwise it is rolled back."""
166 # No nesting of transactions
170 self
.cur
= self
.conn
.cursor()
172 ret
= func(*args
, **kwargs
)
184 def txn_createTables(self
):
186 This method creates the database tables necessary for this
187 store to work. It should not be called if the tables already
190 self
.db_create_nonce()
191 self
.db_create_assoc()
193 createTables
= _inTxn(txn_createTables
)
195 def txn_storeAssociation(self
, server_url
, association
):
196 """Set the association for the server URL.
198 Association -> NoneType
204 self
.blobEncode(a
.secret
),
209 storeAssociation
= _inTxn(txn_storeAssociation
)
211 def txn_getAssociation(self
, server_url
, handle
=None):
212 """Get the most recent association that has been set for this
213 server URL and handle.
215 str -> NoneType or Association
217 if handle
is not None:
218 self
.db_get_assoc(server_url
, handle
)
220 self
.db_get_assocs(server_url
)
222 rows
= self
.cur
.fetchall()
228 assoc
= Association(*values
)
229 assoc
.secret
= self
.blobDecode(assoc
.secret
)
230 if assoc
.getExpiresIn() == 0:
231 self
.txn_removeAssociation(server_url
, assoc
.handle
)
233 associations
.append((assoc
.issued
, assoc
))
237 return associations
[-1][1]
241 getAssociation
= _inTxn(txn_getAssociation
)
243 def txn_removeAssociation(self
, server_url
, handle
):
244 """Remove the association for the given server URL and handle,
245 returning whether the association existed at all.
249 self
.db_remove_assoc(server_url
, handle
)
250 return self
.cur
.rowcount
> 0 # -1 is undefined
252 removeAssociation
= _inTxn(txn_removeAssociation
)
254 def txn_useNonce(self
, server_url
, timestamp
, salt
):
255 """Return whether this nonce is present, and if it is, then
256 remove it from the set.
259 if abs(timestamp
- time
.time()) > nonce
.SKEW
:
263 self
.db_add_nonce(server_url
, timestamp
, salt
)
264 except self
.exceptions
.IntegrityError
:
265 # The key uniqueness check failed
268 # The nonce was successfully added
271 useNonce
= _inTxn(txn_useNonce
)
273 def txn_cleanupNonces(self
):
274 self
.db_clean_nonce(int(time
.time()) - nonce
.SKEW
)
275 return self
.cur
.rowcount
277 cleanupNonces
= _inTxn(txn_cleanupNonces
)
279 def txn_cleanupAssociations(self
):
280 self
.db_clean_assoc(int(time
.time()))
281 return self
.cur
.rowcount
283 cleanupAssociations
= _inTxn(txn_cleanupAssociations
)
286 class SQLiteStore(SQLStore
):
288 This is an SQLite-based specialization of C{L{SQLStore}}.
290 To create an instance, see C{L{SQLStore.__init__}}. To create the
291 tables it will use, see C{L{SQLStore.createTables}}.
293 All other methods are implementation details.
296 create_nonce_sql
= """
297 CREATE TABLE %(nonces)s (
301 UNIQUE(server_url, timestamp, salt)
305 create_assoc_sql
= """
306 CREATE TABLE %(associations)s
308 server_url VARCHAR(2047),
313 assoc_type VARCHAR(64),
314 PRIMARY KEY (server_url, handle)
318 set_assoc_sql
= ('INSERT OR REPLACE INTO %(associations)s '
319 '(server_url, handle, secret, issued, '
320 'lifetime, assoc_type) '
321 'VALUES (?, ?, ?, ?, ?, ?);')
322 get_assocs_sql
= ('SELECT handle, secret, issued, lifetime, assoc_type '
323 'FROM %(associations)s WHERE server_url = ?;')
325 'SELECT handle, secret, issued, lifetime, assoc_type '
326 'FROM %(associations)s WHERE server_url = ? AND handle = ?;')
328 get_expired_sql
= ('SELECT server_url '
329 'FROM %(associations)s WHERE issued + lifetime < ?;')
331 remove_assoc_sql
= ('DELETE FROM %(associations)s '
332 'WHERE server_url = ? AND handle = ?;')
334 clean_assoc_sql
= 'DELETE FROM %(associations)s WHERE issued + lifetime < ?;'
336 add_nonce_sql
= 'INSERT INTO %(nonces)s VALUES (?, ?, ?);'
338 clean_nonce_sql
= 'DELETE FROM %(nonces)s WHERE timestamp < ?;'
340 def blobDecode(self
, buf
):
343 def blobEncode(self
, s
):
346 def useNonce(self
, *args
, **kwargs
):
347 # Older versions of the sqlite wrapper do not raise
348 # IntegrityError as they should, so we have to detect the
349 # message from the OperationalError.
351 return super(SQLiteStore
, self
).useNonce(*args
, **kwargs
)
352 except self
.exceptions
.OperationalError
, why
:
353 if re
.match('^columns .* are not unique$', why
[0]):
358 class MySQLStore(SQLStore
):
360 This is a MySQL-based specialization of C{L{SQLStore}}.
362 Uses InnoDB tables for transaction support.
364 To create an instance, see C{L{SQLStore.__init__}}. To create the
365 tables it will use, see C{L{SQLStore.createTables}}.
367 All other methods are implementation details.
371 import MySQLdb
as exceptions
375 create_nonce_sql
= """
376 CREATE TABLE %(nonces)s (
377 server_url BLOB NOT NULL,
378 timestamp INTEGER NOT NULL,
379 salt CHAR(40) NOT NULL,
380 PRIMARY KEY (server_url(255), timestamp, salt)
385 create_assoc_sql
= """
386 CREATE TABLE %(associations)s
388 server_url BLOB NOT NULL,
389 handle VARCHAR(255) NOT NULL,
390 secret BLOB NOT NULL,
391 issued INTEGER NOT NULL,
392 lifetime INTEGER NOT NULL,
393 assoc_type VARCHAR(64) NOT NULL,
394 PRIMARY KEY (server_url(255), handle)
399 set_assoc_sql
= ('REPLACE INTO %(associations)s '
400 'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);')
401 get_assocs_sql
= ('SELECT handle, secret, issued, lifetime, assoc_type'
402 ' FROM %(associations)s WHERE server_url = %%s;')
403 get_expired_sql
= ('SELECT server_url '
404 'FROM %(associations)s WHERE issued + lifetime < %%s;')
407 'SELECT handle, secret, issued, lifetime, assoc_type'
408 ' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;')
409 remove_assoc_sql
= ('DELETE FROM %(associations)s '
410 'WHERE server_url = %%s AND handle = %%s;')
412 clean_assoc_sql
= 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;'
414 add_nonce_sql
= 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);'
416 clean_nonce_sql
= 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
418 def blobDecode(self
, blob
):
419 if type(blob
) is str:
420 # Versions of MySQLdb >= 1.2.2
423 # Versions of MySQLdb prior to 1.2.2 (as far as we can tell)
424 return blob
.tostring()
426 class PostgreSQLStore(SQLStore
):
428 This is a PostgreSQL-based specialization of C{L{SQLStore}}.
430 To create an instance, see C{L{SQLStore.__init__}}. To create the
431 tables it will use, see C{L{SQLStore.createTables}}.
433 All other methods are implementation details.
437 import psycopg
as exceptions
439 # psycopg2 has the dbapi extension where the exception classes
440 # are available on the connection object. A psycopg2
441 # connection will use the correct exception classes because of
442 # this, and a psycopg connection will fall through to use the
443 # psycopg imported above.
446 create_nonce_sql
= """
447 CREATE TABLE %(nonces)s (
448 server_url VARCHAR(2047) NOT NULL,
449 timestamp INTEGER NOT NULL,
450 salt CHAR(40) NOT NULL,
451 PRIMARY KEY (server_url, timestamp, salt)
455 create_assoc_sql
= """
456 CREATE TABLE %(associations)s
458 server_url VARCHAR(2047) NOT NULL,
459 handle VARCHAR(255) NOT NULL,
460 secret BYTEA NOT NULL,
461 issued INTEGER NOT NULL,
462 lifetime INTEGER NOT NULL,
463 assoc_type VARCHAR(64) NOT NULL,
464 PRIMARY KEY (server_url, handle),
465 CONSTRAINT secret_length_constraint CHECK (LENGTH(secret) <= 128)
469 def db_set_assoc(self
, server_url
, handle
, secret
, issued
, lifetime
, assoc_type
):
471 Set an association. This is implemented as a method because
472 REPLACE INTO is not supported by PostgreSQL (and is not
475 result
= self
.db_get_assoc(server_url
, handle
)
476 rows
= self
.cur
.fetchall()
478 # Update the table since this associations already exists.
479 return self
.db_update_assoc(secret
, issued
, lifetime
, assoc_type
,
482 # Insert a new record because this association wasn't
484 return self
.db_new_assoc(server_url
, handle
, secret
, issued
,
485 lifetime
, assoc_type
)
487 new_assoc_sql
= ('INSERT INTO %(associations)s '
488 'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);')
489 update_assoc_sql
= ('UPDATE %(associations)s SET '
490 'secret = %%s, issued = %%s, '
491 'lifetime = %%s, assoc_type = %%s '
492 'WHERE server_url = %%s AND handle = %%s;')
493 get_assocs_sql
= ('SELECT handle, secret, issued, lifetime, assoc_type'
494 ' FROM %(associations)s WHERE server_url = %%s;')
495 get_expired_sql
= ('SELECT server_url '
496 'FROM %(associations)s WHERE issued + lifetime < %%s;')
499 'SELECT handle, secret, issued, lifetime, assoc_type'
500 ' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;')
501 remove_assoc_sql
= ('DELETE FROM %(associations)s '
502 'WHERE server_url = %%s AND handle = %%s;')
504 clean_assoc_sql
= 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;'
506 add_nonce_sql
= 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);'
508 clean_nonce_sql
= 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
510 def blobEncode(self
, blob
):
512 from psycopg2
import Binary
514 from psycopg
import Binary