sqlstore.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. """
  2. This module contains C{L{OpenIDStore}} implementations that use
  3. various SQL databases to back them.
  4. Example of how to initialize a store database::
  5. python -c 'from openid.store import sqlstore; import pysqlite2.dbapi2; sqlstore.SQLiteStore(pysqlite2.dbapi2.connect("cstore.db")).createTables()'
  6. """
  7. import re
  8. import time
  9. from openid.association import Association
  10. from openid.store.interface import OpenIDStore
  11. from openid.store import nonce
  12. def _inTxn(func):
  13. def wrapped(self, *args, **kwargs):
  14. return self._callInTransaction(func, self, *args, **kwargs)
  15. if hasattr(func, '__name__'):
  16. try:
  17. wrapped.__name__ = func.__name__[4:]
  18. except TypeError:
  19. pass
  20. if hasattr(func, '__doc__'):
  21. wrapped.__doc__ = func.__doc__
  22. return wrapped
  23. class SQLStore(OpenIDStore):
  24. """
  25. This is the parent class for the SQL stores, which contains the
  26. logic common to all of the SQL stores.
  27. The table names used are determined by the class variables
  28. C{L{associations_table}} and
  29. C{L{nonces_table}}. To change the name of the tables used, pass
  30. new table names into the constructor.
  31. To create the tables with the proper schema, see the
  32. C{L{createTables}} method.
  33. This class shouldn't be used directly. Use one of its subclasses
  34. instead, as those contain the code necessary to use a specific
  35. database.
  36. All methods other than C{L{__init__}} and C{L{createTables}}
  37. should be considered implementation details.
  38. @cvar associations_table: This is the default name of the table to
  39. keep associations in
  40. @cvar nonces_table: This is the default name of the table to keep
  41. nonces in.
  42. @sort: __init__, createTables
  43. """
  44. associations_table = 'oid_associations'
  45. nonces_table = 'oid_nonces'
  46. def __init__(self, conn, associations_table=None, nonces_table=None):
  47. """
  48. This creates a new SQLStore instance. It requires an
  49. established database connection be given to it, and it allows
  50. overriding the default table names.
  51. @param conn: This must be an established connection to a
  52. database of the correct type for the SQLStore subclass
  53. you're using.
  54. @type conn: A python database API compatible connection
  55. object.
  56. @param associations_table: This is an optional parameter to
  57. specify the name of the table used for storing
  58. associations. The default value is specified in
  59. C{L{SQLStore.associations_table}}.
  60. @type associations_table: C{str}
  61. @param nonces_table: This is an optional parameter to specify
  62. the name of the table used for storing nonces. The
  63. default value is specified in C{L{SQLStore.nonces_table}}.
  64. @type nonces_table: C{str}
  65. """
  66. self.conn = conn
  67. self.cur = None
  68. self._statement_cache = {}
  69. self._table_names = {
  70. 'associations': associations_table or self.associations_table,
  71. 'nonces': nonces_table or self.nonces_table,
  72. }
  73. self.max_nonce_age = 6 * 60 * 60 # Six hours, in seconds
  74. # DB API extension: search for "Connection Attributes .Error,
  75. # .ProgrammingError, etc." in
  76. # http://www.python.org/dev/peps/pep-0249/
  77. if (hasattr(self.conn, 'IntegrityError') and
  78. hasattr(self.conn, 'OperationalError')):
  79. self.exceptions = self.conn
  80. if not (hasattr(self.exceptions, 'IntegrityError') and
  81. hasattr(self.exceptions, 'OperationalError')):
  82. raise RuntimeError("Error using database connection module "
  83. "(Maybe it can't be imported?)")
  84. def blobDecode(self, blob):
  85. """Convert a blob as returned by the SQL engine into a str object.
  86. str -> str"""
  87. return blob
  88. def blobEncode(self, s):
  89. """Convert a str object into the necessary object for storing
  90. in the database as a blob."""
  91. return s
  92. def _getSQL(self, sql_name):
  93. try:
  94. return self._statement_cache[sql_name]
  95. except KeyError:
  96. sql = getattr(self, sql_name)
  97. sql %= self._table_names
  98. self._statement_cache[sql_name] = sql
  99. return sql
  100. def _execSQL(self, sql_name, *args):
  101. sql = self._getSQL(sql_name)
  102. # Kludge because we have reports of postgresql not quoting
  103. # arguments if they are passed in as unicode instead of str.
  104. # Currently the strings in our tables just have ascii in them,
  105. # so this ought to be safe.
  106. def unicode_to_str(arg):
  107. if isinstance(arg, unicode):
  108. return str(arg)
  109. else:
  110. return arg
  111. str_args = map(unicode_to_str, args)
  112. self.cur.execute(sql, str_args)
  113. def __getattr__(self, attr):
  114. # if the attribute starts with db_, use a default
  115. # implementation that looks up the appropriate SQL statement
  116. # as an attribute of this object and executes it.
  117. if attr[:3] == 'db_':
  118. sql_name = attr[3:] + '_sql'
  119. def func(*args):
  120. return self._execSQL(sql_name, *args)
  121. setattr(self, attr, func)
  122. return func
  123. else:
  124. raise AttributeError('Attribute %r not found' % (attr,))
  125. def _callInTransaction(self, func, *args, **kwargs):
  126. """Execute the given function inside of a transaction, with an
  127. open cursor. If no exception is raised, the transaction is
  128. comitted, otherwise it is rolled back."""
  129. # No nesting of transactions
  130. self.conn.rollback()
  131. try:
  132. self.cur = self.conn.cursor()
  133. try:
  134. ret = func(*args, **kwargs)
  135. finally:
  136. self.cur.close()
  137. self.cur = None
  138. except:
  139. self.conn.rollback()
  140. raise
  141. else:
  142. self.conn.commit()
  143. return ret
  144. def txn_createTables(self):
  145. """
  146. This method creates the database tables necessary for this
  147. store to work. It should not be called if the tables already
  148. exist.
  149. """
  150. self.db_create_nonce()
  151. self.db_create_assoc()
  152. createTables = _inTxn(txn_createTables)
  153. def txn_storeAssociation(self, server_url, association):
  154. """Set the association for the server URL.
  155. Association -> NoneType
  156. """
  157. a = association
  158. self.db_set_assoc(
  159. server_url,
  160. a.handle,
  161. self.blobEncode(a.secret),
  162. a.issued,
  163. a.lifetime,
  164. a.assoc_type)
  165. storeAssociation = _inTxn(txn_storeAssociation)
  166. def txn_getAssociation(self, server_url, handle=None):
  167. """Get the most recent association that has been set for this
  168. server URL and handle.
  169. str -> NoneType or Association
  170. """
  171. if handle is not None:
  172. self.db_get_assoc(server_url, handle)
  173. else:
  174. self.db_get_assocs(server_url)
  175. rows = self.cur.fetchall()
  176. if len(rows) == 0:
  177. return None
  178. else:
  179. associations = []
  180. for values in rows:
  181. assoc = Association(*values)
  182. assoc.secret = self.blobDecode(assoc.secret)
  183. if assoc.getExpiresIn() == 0:
  184. self.txn_removeAssociation(server_url, assoc.handle)
  185. else:
  186. associations.append((assoc.issued, assoc))
  187. if associations:
  188. associations.sort()
  189. return associations[-1][1]
  190. else:
  191. return None
  192. getAssociation = _inTxn(txn_getAssociation)
  193. def txn_removeAssociation(self, server_url, handle):
  194. """Remove the association for the given server URL and handle,
  195. returning whether the association existed at all.
  196. (str, str) -> bool
  197. """
  198. self.db_remove_assoc(server_url, handle)
  199. return self.cur.rowcount > 0 # -1 is undefined
  200. removeAssociation = _inTxn(txn_removeAssociation)
  201. def txn_useNonce(self, server_url, timestamp, salt):
  202. """Return whether this nonce is present, and if it is, then
  203. remove it from the set.
  204. str -> bool"""
  205. if abs(timestamp - time.time()) > nonce.SKEW:
  206. return False
  207. try:
  208. self.db_add_nonce(server_url, timestamp, salt)
  209. except self.exceptions.IntegrityError:
  210. # The key uniqueness check failed
  211. return False
  212. else:
  213. # The nonce was successfully added
  214. return True
  215. useNonce = _inTxn(txn_useNonce)
  216. def txn_cleanupNonces(self):
  217. self.db_clean_nonce(int(time.time()) - nonce.SKEW)
  218. return self.cur.rowcount
  219. cleanupNonces = _inTxn(txn_cleanupNonces)
  220. def txn_cleanupAssociations(self):
  221. self.db_clean_assoc(int(time.time()))
  222. return self.cur.rowcount
  223. cleanupAssociations = _inTxn(txn_cleanupAssociations)
  224. class SQLiteStore(SQLStore):
  225. """
  226. This is an SQLite-based specialization of C{L{SQLStore}}.
  227. To create an instance, see C{L{SQLStore.__init__}}. To create the
  228. tables it will use, see C{L{SQLStore.createTables}}.
  229. All other methods are implementation details.
  230. """
  231. create_nonce_sql = """
  232. CREATE TABLE %(nonces)s (
  233. server_url VARCHAR,
  234. timestamp INTEGER,
  235. salt CHAR(40),
  236. UNIQUE(server_url, timestamp, salt)
  237. );
  238. """
  239. create_assoc_sql = """
  240. CREATE TABLE %(associations)s
  241. (
  242. server_url VARCHAR(2047),
  243. handle VARCHAR(255),
  244. secret BLOB(128),
  245. issued INTEGER,
  246. lifetime INTEGER,
  247. assoc_type VARCHAR(64),
  248. PRIMARY KEY (server_url, handle)
  249. );
  250. """
  251. set_assoc_sql = ('INSERT OR REPLACE INTO %(associations)s '
  252. '(server_url, handle, secret, issued, '
  253. 'lifetime, assoc_type) '
  254. 'VALUES (?, ?, ?, ?, ?, ?);')
  255. get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type '
  256. 'FROM %(associations)s WHERE server_url = ?;')
  257. get_assoc_sql = (
  258. 'SELECT handle, secret, issued, lifetime, assoc_type '
  259. 'FROM %(associations)s WHERE server_url = ? AND handle = ?;')
  260. get_expired_sql = ('SELECT server_url '
  261. 'FROM %(associations)s WHERE issued + lifetime < ?;')
  262. remove_assoc_sql = ('DELETE FROM %(associations)s '
  263. 'WHERE server_url = ? AND handle = ?;')
  264. clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < ?;'
  265. add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (?, ?, ?);'
  266. clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < ?;'
  267. def blobDecode(self, buf):
  268. return str(buf)
  269. def blobEncode(self, s):
  270. return buffer(s)
  271. def useNonce(self, *args, **kwargs):
  272. # Older versions of the sqlite wrapper do not raise
  273. # IntegrityError as they should, so we have to detect the
  274. # message from the OperationalError.
  275. try:
  276. return super(SQLiteStore, self).useNonce(*args, **kwargs)
  277. except self.exceptions.OperationalError, why:
  278. if re.match('^columns .* are not unique$', why[0]):
  279. return False
  280. else:
  281. raise
  282. class MySQLStore(SQLStore):
  283. """
  284. This is a MySQL-based specialization of C{L{SQLStore}}.
  285. Uses InnoDB tables for transaction support.
  286. To create an instance, see C{L{SQLStore.__init__}}. To create the
  287. tables it will use, see C{L{SQLStore.createTables}}.
  288. All other methods are implementation details.
  289. """
  290. try:
  291. import MySQLdb as exceptions
  292. except ImportError:
  293. exceptions = None
  294. create_nonce_sql = """
  295. CREATE TABLE %(nonces)s (
  296. server_url BLOB NOT NULL,
  297. timestamp INTEGER NOT NULL,
  298. salt CHAR(40) NOT NULL,
  299. PRIMARY KEY (server_url(255), timestamp, salt)
  300. )
  301. ENGINE=InnoDB;
  302. """
  303. create_assoc_sql = """
  304. CREATE TABLE %(associations)s
  305. (
  306. server_url BLOB NOT NULL,
  307. handle VARCHAR(255) NOT NULL,
  308. secret BLOB NOT NULL,
  309. issued INTEGER NOT NULL,
  310. lifetime INTEGER NOT NULL,
  311. assoc_type VARCHAR(64) NOT NULL,
  312. PRIMARY KEY (server_url(255), handle)
  313. )
  314. ENGINE=InnoDB;
  315. """
  316. set_assoc_sql = ('REPLACE INTO %(associations)s '
  317. 'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);')
  318. get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type'
  319. ' FROM %(associations)s WHERE server_url = %%s;')
  320. get_expired_sql = ('SELECT server_url '
  321. 'FROM %(associations)s WHERE issued + lifetime < %%s;')
  322. get_assoc_sql = (
  323. 'SELECT handle, secret, issued, lifetime, assoc_type'
  324. ' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;')
  325. remove_assoc_sql = ('DELETE FROM %(associations)s '
  326. 'WHERE server_url = %%s AND handle = %%s;')
  327. clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;'
  328. add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);'
  329. clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
  330. def blobDecode(self, blob):
  331. if type(blob) is str:
  332. # Versions of MySQLdb >= 1.2.2
  333. return blob
  334. else:
  335. # Versions of MySQLdb prior to 1.2.2 (as far as we can tell)
  336. return blob.tostring()
  337. class PostgreSQLStore(SQLStore):
  338. """
  339. This is a PostgreSQL-based specialization of C{L{SQLStore}}.
  340. To create an instance, see C{L{SQLStore.__init__}}. To create the
  341. tables it will use, see C{L{SQLStore.createTables}}.
  342. All other methods are implementation details.
  343. """
  344. try:
  345. import psycopg as exceptions
  346. except ImportError:
  347. # psycopg2 has the dbapi extension where the exception classes
  348. # are available on the connection object. A psycopg2
  349. # connection will use the correct exception classes because of
  350. # this, and a psycopg connection will fall through to use the
  351. # psycopg imported above.
  352. exceptions = None
  353. create_nonce_sql = """
  354. CREATE TABLE %(nonces)s (
  355. server_url VARCHAR(2047) NOT NULL,
  356. timestamp INTEGER NOT NULL,
  357. salt CHAR(40) NOT NULL,
  358. PRIMARY KEY (server_url, timestamp, salt)
  359. );
  360. """
  361. create_assoc_sql = """
  362. CREATE TABLE %(associations)s
  363. (
  364. server_url VARCHAR(2047) NOT NULL,
  365. handle VARCHAR(255) NOT NULL,
  366. secret BYTEA NOT NULL,
  367. issued INTEGER NOT NULL,
  368. lifetime INTEGER NOT NULL,
  369. assoc_type VARCHAR(64) NOT NULL,
  370. PRIMARY KEY (server_url, handle),
  371. CONSTRAINT secret_length_constraint CHECK (LENGTH(secret) <= 128)
  372. );
  373. """
  374. def db_set_assoc(self, server_url, handle, secret, issued, lifetime, assoc_type):
  375. """
  376. Set an association. This is implemented as a method because
  377. REPLACE INTO is not supported by PostgreSQL (and is not
  378. standard SQL).
  379. """
  380. result = self.db_get_assoc(server_url, handle)
  381. rows = self.cur.fetchall()
  382. if len(rows):
  383. # Update the table since this associations already exists.
  384. return self.db_update_assoc(secret, issued, lifetime, assoc_type,
  385. server_url, handle)
  386. else:
  387. # Insert a new record because this association wasn't
  388. # found.
  389. return self.db_new_assoc(server_url, handle, secret, issued,
  390. lifetime, assoc_type)
  391. new_assoc_sql = ('INSERT INTO %(associations)s '
  392. 'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);')
  393. update_assoc_sql = ('UPDATE %(associations)s SET '
  394. 'secret = %%s, issued = %%s, '
  395. 'lifetime = %%s, assoc_type = %%s '
  396. 'WHERE server_url = %%s AND handle = %%s;')
  397. get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type'
  398. ' FROM %(associations)s WHERE server_url = %%s;')
  399. get_expired_sql = ('SELECT server_url '
  400. 'FROM %(associations)s WHERE issued + lifetime < %%s;')
  401. get_assoc_sql = (
  402. 'SELECT handle, secret, issued, lifetime, assoc_type'
  403. ' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;')
  404. remove_assoc_sql = ('DELETE FROM %(associations)s '
  405. 'WHERE server_url = %%s AND handle = %%s;')
  406. clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;'
  407. add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);'
  408. clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
  409. def blobEncode(self, blob):
  410. try:
  411. from psycopg2 import Binary
  412. except ImportError:
  413. from psycopg import Binary
  414. return Binary(blob)