postgresql.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. # ============================================================================
  2. # This file is part of Pwman3.
  3. #
  4. # Pwman3 is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License, version 2
  6. # as published by the Free Software Foundation;
  7. #
  8. # Pwman3 is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with Pwman3; if not, write to the Free Software
  15. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  16. # ============================================================================
  17. # Copyright (C) 2012 Oz Nahum <nahumoz@gmail.com>
  18. # ============================================================================
  19. # Copyright (C) 2006 Ivan Kelly <ivan@ivankelly.net>
  20. # ============================================================================
  21. """Postgresql Database implementation."""
  22. import sys
  23. if sys.version_info.major > 2: # pragma: no cover
  24. from urllib import urlparse
  25. else:
  26. from urlparse import urlparse
  27. import psycopg2 as pg
  28. import cPickle
  29. import pwman.util.config as config
  30. from pwman.data.database import Database, DatabaseException, __DB_FORMAT__
  31. class PostgresqlDatabase(Database):
  32. """
  33. Postgresql Database implementation
  34. This assumes that your database admin has created a pwman database
  35. for you and shared the user name and password with you.
  36. This driver send no clear text on wire. ONLY excrypted stuff is sent
  37. between the client and the server.
  38. Encryption and decryption are happening on your localhost, not on
  39. the Postgresql server.
  40. create table version
  41. CREATE TABLE DB_VERSION(DBVERSION TEXT NOT NULL DEFAULT '');
  42. Check if db_version exists
  43. SELECT 1
  44. FROM information_schema.tables where table_name = 'db_version';
  45. get information:
  46. SELECT dbversion FROM DB_VERSION;
  47. """
  48. @classmethod
  49. def check_db_version(cls, user, dbname='pwman'):
  50. """
  51. Check the database version
  52. """
  53. con = pg.connect("dbname=pwman user=%s" % user)
  54. cur = con.cursor()
  55. try:
  56. cur.execute("SELECT VERSION from DBVERSION")
  57. version = cur.fetchone()
  58. return version
  59. except pg.ProgrammingError:
  60. con.rollback()
  61. raise DatabaseException("Something seems fishy with the DB")
  62. def __init__(self, pgsqluri, dbformat=__DB_FORMAT__):
  63. """
  64. Initialise PostgresqlDatabase instance.
  65. """
  66. self._pgsqluri = pgsqluri
  67. self.dbversion = dbformat
  68. def _open(self):
  69. u = urlparse(self._pgsqluri)
  70. self._con = pg.connect(database=u.path[1:], user=u.username,
  71. password=u.password, host=u.hostname)
  72. self._cur = self._con.cursor()
  73. self._create_tables()
  74. def close(self):
  75. # TODO: implement _clean_orphands
  76. self._cur.close()
  77. self._con.close()
  78. def listtags(self, all=False):
  79. sql = ''
  80. params = {}
  81. i = 0
  82. if len(self._filtertags) == 0 or all:
  83. sql = "SELECT DATA FROM %sTAGS ORDER BY DATA ASC" % (self._prefix)
  84. else:
  85. sql = ("SELECT %sTAGS.DATA FROM %sLOOKUP"
  86. + " INNER JOIN %sTAGS ON %sLOOKUP.TAG = %sTAGS.ID"
  87. + " WHERE NODE IN (") % (self._prefix, self._prefix,
  88. self._prefix,
  89. self._prefix, self._prefix)
  90. first = True
  91. i += 1
  92. paramname = "tag%d" % (i)
  93. for t in self._filtertags:
  94. if not first:
  95. sql += " INTERSECT "
  96. else:
  97. first = False
  98. sql += ("SELECT NODE FROM %sLOOKUP LEFT JOIN %sTAGS ON TAG "
  99. "= %sTAGS.ID "
  100. " WHERE %sTAGS.DATA = %%(%s)s" % (self._prefix,
  101. self._prefix,
  102. self._prefix,
  103. self._prefix,
  104. paramname))
  105. params[paramname] = cPickle.dumps(t)
  106. sql += ") EXCEPT SELECT DATA FROM %sTAGS WHERE " % self._prefix
  107. first = True
  108. for t in self._filtertags:
  109. if not first:
  110. sql += " OR "
  111. else:
  112. first = False
  113. sql += "%sTAGS.DATA = %%(%s)s" % (self._prefix, paramname)
  114. try:
  115. cursor = self._get_cur()
  116. cursor.execute(sql, params)
  117. tags = []
  118. row = cursor.fetchone()
  119. while row:
  120. tag = cPickle.loads(str(row[0]))
  121. tags.append(tag)
  122. row = cursor.fetchone()
  123. return tags
  124. except pgdb.DatabaseError as e:
  125. raise DatabaseException("Postgresql: %s" % (e))
  126. def getnodes(self, ids):
  127. nodes = []
  128. idstr = ""
  129. first = True
  130. if len(ids) == 0:
  131. idstr = "-1"
  132. for i in ids:
  133. if first:
  134. idstr += "%d" % (i)
  135. first = False
  136. else:
  137. idstr += ", %d" % (i)
  138. sql = ("SELECT ID,DATA FROM %sNODES WHERE ID IN (%s)"
  139. "" % (self._prefix, pgdb.escape_string(idstr)))
  140. try:
  141. cursor = self._get_cur()
  142. cursor.execute(sql)
  143. row = cursor.fetchone()
  144. while row:
  145. node = cPickle.loads(str(row[1]))
  146. node.set_id(row[0])
  147. nodes.append(node)
  148. row = cursor.fetchone()
  149. except pgdb.DatabaseError as e:
  150. raise DatabaseException("Postgresql: %s" % (e))
  151. return nodes
  152. def editnode(self, id, node):
  153. if not isinstance(node, Node):
  154. raise DatabaseException("Tried to insert foreign object into "
  155. "database [%s]" % node)
  156. try:
  157. cursor = self._get_cur()
  158. sql = ("UPDATE %sNODES SET DATA = %%(data)s WHERE ID = %%(id)d"
  159. "" % (self._prefix))
  160. cursor.execute(sql, {"data": cPickle.dumps(node),
  161. "id": id})
  162. except pgdb.DatabaseError as e:
  163. raise DatabaseException("Postgresql: %s" % (e))
  164. self._setnodetags(node)
  165. self._checktags()
  166. self._commit()
  167. def add_node(self, node):
  168. sql = ("INSERT INTO NODE(USERNAME, PASSWORD, URL, NOTES)"
  169. "VALUES(%s, %s, %s, %s)")
  170. node_tags = list(node)
  171. node, tags = node_tags[:4], node_tags[-1]
  172. self._cur.execute(sql, (node))
  173. #self._setnodetags(self._cur.lastrowid, tags)
  174. self._con.commit()
  175. def removenodes(self, nodes):
  176. cursor = self._get_cur()
  177. for n in nodes:
  178. if not isinstance(n, Node):
  179. raise DatabaseException("Tried to delete foreign object"
  180. "from database [%s]", n)
  181. try:
  182. sql = "DELETE FROM %sNODES WHERE ID = %%(id)d" % (self._prefix)
  183. cursor.execute(sql, {"id": n.get_id()})
  184. except pgdb.DatabaseError as e:
  185. raise DatabaseException("Postgresql: %s" % (e))
  186. self._deletenodetags(n)
  187. self._checktags()
  188. self._commit()
  189. def listnodes(self):
  190. sql = ''
  191. params = {}
  192. i = 0
  193. cursor = self._get_cur()
  194. if len(self._filtertags) == 0:
  195. sql = "SELECT ID FROM %sNODES ORDER BY ID ASC" % (self._prefix)
  196. else:
  197. first = True
  198. for t in self._filtertags:
  199. if not first:
  200. sql += " INTERSECT "
  201. else:
  202. first = False
  203. i += 1
  204. paramname = "tag%d" % (i)
  205. sql += (("SELECT NODE FROM %sLOOKUP LEFT JOIN %sTAGS ON"
  206. " TAG = %sTAGS.ID"
  207. " WHERE %sTAGS.DATA = %%(%s)s ") % (self._prefix,
  208. self._prefix,
  209. self._prefix,
  210. self._prefix,
  211. paramname))
  212. params[paramname] = cPickle.dumps(t)
  213. try:
  214. cursor.execute(sql, params)
  215. ids = []
  216. row = cursor.fetchone()
  217. while row:
  218. ids.append(row[0])
  219. row = cursor.fetchone()
  220. return ids
  221. except pgdb.DatabaseError as e:
  222. raise DatabaseException("Postgresql: %s" % (e))
  223. def _commit(self):
  224. try:
  225. self._con.commit()
  226. except pgdb.DatabaseError as e:
  227. self._con.rollback()
  228. raise DatabaseException(
  229. "Postgresql: Error commiting data to db [%s]" % (e))
  230. def _tagids(self, tags):
  231. ids = []
  232. cursor = self._get_cur()
  233. for t in tags:
  234. pickled = cPickle.dumps(t)
  235. try:
  236. ids.append(self._tagidcache[pickled])
  237. continue
  238. except KeyError as e:
  239. pass # not in cache
  240. sql = "SELECT ID FROM %sTAGS WHERE DATA = %%(tag)s" % (
  241. self._prefix)
  242. if not isinstance(t, Tag):
  243. raise DatabaseException("Tried to insert foreign object"
  244. " into database [%s]", t)
  245. data = {"tag": pickled}
  246. try:
  247. cursor.execute(sql, data)
  248. row = cursor.fetchone()
  249. if row:
  250. ids.append(row[0])
  251. self._tagidcache[pickled] = row[0]
  252. else:
  253. sql = "INSERT INTO %sTAGS(DATA) VALUES(%%(tag)s)" % (
  254. self._prefix)
  255. cursor.execute(sql, data)
  256. id = self._lastrowid("TAGS")
  257. ids.append(id)
  258. self._tagidcache[pickled] = id
  259. except pgdb.DatabaseError as e:
  260. raise DatabaseException("Postgresql: %s" % (e))
  261. return ids
  262. def _deletenodetags(self, node):
  263. try:
  264. cursor = self._get_cur()
  265. sql = "DELETE FROM %sLOOKUP WHERE NODE = %%(node)d" % (
  266. self._prefix)
  267. cursor.execute(sql, {"node": node.get_id()})
  268. except pgdb.DatabaseError as e:
  269. raise DatabaseException("Postgresql: %s" % (e))
  270. def _setnodetags(self, node):
  271. self._deletenodetags(node)
  272. ids = self._tagids(node.get_tags())
  273. for i in ids:
  274. sql = "INSERT INTO %sLOOKUP VALUES(%%(node)d, %%(tag)d)" % (
  275. self._prefix)
  276. params = {"node": node.get_id(), "tag": i}
  277. try:
  278. cursor = self._get_cur()
  279. cursor.execute(sql, params)
  280. except pgdb.DatabaseError as e:
  281. raise DatabaseException("Postgresql: %s" % (e))
  282. def _checktags(self):
  283. self._tagidcache.clear()
  284. try:
  285. cursor = self._get_cur()
  286. sql = ("DELETE FROM %sTAGS WHERE ID NOT IN "
  287. + "(SELECT TAG FROM %sLOOKUP GROUP BY TAG)") % (self._prefix,
  288. self._prefix)
  289. cursor.execute(sql)
  290. except pgdb.DatabaseError as e:
  291. raise DatabaseException("Postgresql: %s" % (e))
  292. self._commit()
  293. def _lastrowid(self, name):
  294. cursor = self._get_cur()
  295. cursor.execute("SELECT LAST_VALUE FROM %s%s_ID_SEQ" % (self._prefix,
  296. name))
  297. row = cursor.fetchone()
  298. if not row:
  299. return 0
  300. else:
  301. return row[0]
  302. def _create_tables(self):
  303. try:
  304. self._cur.execute("SELECT 1 from DBVERSION")
  305. version = self._cur.fetchone()
  306. if version:
  307. return
  308. except pg.ProgrammingError:
  309. self._con.rollback()
  310. try:
  311. self._cur.execute("CREATE TABLE NODE(ID SERIAL PRIMARY KEY, "
  312. "USERNAME TEXT NOT NULL, "
  313. "PASSWORD TEXT NOT NULL, "
  314. "URL TEXT NOT NULL, "
  315. "NOTES TEXT NOT NULL"
  316. ")")
  317. self._cur.execute("CREATE TABLE TAG"
  318. "(ID SERIAL PRIMARY KEY,"
  319. "DATA TEXT NOT NULL UNIQUE)")
  320. self._cur.execute("CREATE TABLE LOOKUP ("
  321. "nodeid SERIAL REFERENCES NODE(ID),"
  322. "tagid SERIAL REFERENCES TAG(ID)"
  323. ")")
  324. self._cur.execute("CREATE TABLE CRYPTO "
  325. "(SEED TEXT, DIGEST TEXT)")
  326. self._cur.execute("CREATE TABLE DBVERSION("
  327. "VERSION TEXT NOT NULL DEFAULT {}"
  328. ")".format(__DB_FORMAT__))
  329. self._cur.execute("INSERT INTO DBVERSION VALUES(%s)",
  330. (self.dbversion,))
  331. self._con.commit()
  332. except pg.ProgrammingError:
  333. self._con.rollback()
  334. def save_crypto_info(self, seed, digest):
  335. """save the random seed and the digested key"""
  336. self._cur.execute("DELETE FROM CRYPTO")
  337. self._cur.execute("INSERT INTO CRYPTO VALUES(%s, %s)", (seed, digest))
  338. self._con.commit()
  339. def savekey(self, key):
  340. salt, digest = key.split('$6$')
  341. sql = "INSERT INTO CRYPTO(SEED, DIGEST) VALUES(%s,%s)"
  342. self._cur.execute("DELETE FROM CRYPTO")
  343. self._cur.execute(sql, (salt, digest))
  344. self._digest = digest.encode('utf-8')
  345. self._salt = salt.encode('utf-8')
  346. self._con.commit()
  347. def loadkey(self):
  348. sql = "SELECT * FROM CRYPTO"
  349. try:
  350. self._cur.execute(sql)
  351. seed, digest = self._cur.fetchone()
  352. return seed + u'$6$' + digest
  353. except TypeError:
  354. return None