Mercurial > libervia-pubsub
comparison sat_pubsub/pgsql_storage.py @ 330:82d1259b3e36
backend, pgsql storage: better items/notification handling, various fixes:
- replaced const.VAL_AMODEL_ROSTER by const.VAL_AMODEL_PUBLISHER_ROSTER to follow change in pgsql schema
- implemented whitelist access model
- fixed bad access check during items retrieval (access was checked on recipient instead of requestor/sender)
- getItemsData and notification filtering now use inline callbacks: this make these complexe workflows far mor easy to read, and clarity is imperative in these security critical sections.
- publisher-roster access model now need to have only one owner, else it will fail. The idea is to use this model only when owner=publisher, else there is ambiguity on the roster to use to check access
- replaced getNodeOwner by node.getOwners, as a node can have several owners
- notifications filtering has been fixed in a similar way
- psql: simplified withPEP method, pep_table argument is actually not needed
- removed error.NotInRoster: error.Forbidden is used instead
- notifications now notify all the owners, not only the first one
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 26 Mar 2017 20:52:32 +0200 |
parents | e73e42b4f6ff |
children | e93a9fd329d9 |
comparison
equal
deleted
inserted
replaced
329:98409ef42c94 | 330:82d1259b3e36 |
---|---|
53 | 53 |
54 import copy, logging | 54 import copy, logging |
55 | 55 |
56 from zope.interface import implements | 56 from zope.interface import implements |
57 | 57 |
58 from twisted.internet import defer | |
59 from twisted.words.protocols.jabber import jid | 58 from twisted.words.protocols.jabber import jid |
60 from twisted.python import log | 59 from twisted.python import log |
61 | 60 |
62 from wokkel import generic | 61 from wokkel import generic |
63 from wokkel.pubsub import Subscription | 62 from wokkel.pubsub import Subscription |
72 psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) | 71 psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) |
73 psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY) | 72 psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY) |
74 | 73 |
75 # parseXml manage str, but we get unicode | 74 # parseXml manage str, but we get unicode |
76 parseXml = lambda unicode_data: generic.parseXml(unicode_data.encode('utf-8')) | 75 parseXml = lambda unicode_data: generic.parseXml(unicode_data.encode('utf-8')) |
77 | 76 PEP_COL_NAME = 'pep' |
78 | 77 |
79 def withPEP(query, values, pep, recipient, pep_table=None): | 78 |
79 def withPEP(query, values, pep, recipient): | |
80 """Helper method to facilitate PEP management | 80 """Helper method to facilitate PEP management |
81 | 81 |
82 @param query: SQL query basis | 82 @param query: SQL query basis |
83 @param values: current values to replace in query | 83 @param values: current values to replace in query |
84 @param pep: True if we are in PEP mode | 84 @param pep: True if we are in PEP mode |
85 @param recipient: jid of the recipient | 85 @param recipient: jid of the recipient |
86 @param pep_table: added before pep if table need to be specified | |
87 @return: query + PEP AND check, | 86 @return: query + PEP AND check, |
88 recipient's bare jid is added to value if needed | 87 recipient's bare jid is added to value if needed |
89 """ | 88 """ |
90 pep_col_name = "{}pep".format( | |
91 '' if pep_table is None | |
92 else ".{}".format(pep_table)) | |
93 if pep: | 89 if pep: |
94 pep_check="AND {}=%s".format(pep_col_name) | 90 pep_check="AND {}=%s".format(PEP_COL_NAME) |
95 values=list(values) + [recipient.userhost()] | 91 values=list(values) + [recipient.userhost()] |
96 else: | 92 else: |
97 pep_check="AND {} IS NULL".format(pep_col_name) | 93 pep_check="AND {} IS NULL".format(PEP_COL_NAME) |
98 return "{} {}".format(query, pep_check), values | 94 return "{} {}".format(query, pep_check), values |
99 | 95 |
100 | 96 |
101 class Storage: | 97 class Storage: |
102 | 98 |
259 SELECT %s, entity_id, 'owner' FROM | 255 SELECT %s, entity_id, 'owner' FROM |
260 (SELECT entity_id FROM entities | 256 (SELECT entity_id FROM entities |
261 WHERE jid=%s) as e""", | 257 WHERE jid=%s) as e""", |
262 (node_id, owner)) | 258 (node_id, owner)) |
263 | 259 |
264 #TODO: manage JID access | 260 if config[const.OPT_ACCESS_MODEL] == const.VAL_AMODEL_PUBLISHER_ROSTER: |
265 if config[const.OPT_ACCESS_MODEL] == const.VAL_AMODEL_ROSTER: | |
266 if const.OPT_ROSTER_GROUPS_ALLOWED in config: | 261 if const.OPT_ROSTER_GROUPS_ALLOWED in config: |
267 allowed_groups = config[const.OPT_ROSTER_GROUPS_ALLOWED] | 262 allowed_groups = config[const.OPT_ROSTER_GROUPS_ALLOWED] |
268 else: | 263 else: |
269 allowed_groups = [] | 264 allowed_groups = [] |
270 for group in allowed_groups: | 265 for group in allowed_groups: |
271 #TODO: check that group are actually in roster | 266 #TODO: check that group are actually in roster |
272 cursor.execute("""INSERT INTO node_groups_authorized (node_id, groupname) | 267 cursor.execute("""INSERT INTO node_groups_authorized (node_id, groupname) |
273 VALUES (%s,%s)""" , (node_id, group)) | 268 VALUES (%s,%s)""" , (node_id, group)) |
269 # XXX: affiliations can't be set on during node creation (at least not with XEP-0060 alone) | |
270 # so whitelist affiliations need to be done afterward | |
274 | 271 |
275 def deleteNodeByDbId(self, db_id): | 272 def deleteNodeByDbId(self, db_id): |
276 """Delete a node using directly its database id""" | 273 """Delete a node using directly its database id""" |
277 return self.dbpool.runInteraction(self._deleteNodeByDbId, db_id) | 274 return self.dbpool.runInteraction(self._deleteNodeByDbId, db_id) |
278 | 275 |
292 (nodeIdentifier,), pep, recipient)) | 289 (nodeIdentifier,), pep, recipient)) |
293 | 290 |
294 if cursor.rowcount != 1: | 291 if cursor.rowcount != 1: |
295 raise error.NodeNotFound() | 292 raise error.NodeNotFound() |
296 | 293 |
297 def getNodeGroups(self, nodeIdentifier, pep, recipient=None): | 294 |
298 return self.dbpool.runInteraction(self._getNodeGroups, nodeIdentifier, pep, recipient) | |
299 | |
300 def _getNodeGroups(self, cursor, nodeIdentifier, pep, recipient): | |
301 cursor.execute(*withPEP("SELECT groupname FROM node_groups_authorized NATURAL JOIN nodes WHERE node=%s", | |
302 (nodeIdentifier,), pep, recipient)) | |
303 rows = cursor.fetchall() | |
304 | |
305 return [row[0] for row in rows] | |
306 | 295 |
307 def getAffiliations(self, entity, pep, recipient=None): | 296 def getAffiliations(self, entity, pep, recipient=None): |
308 d = self.dbpool.runQuery(*withPEP("""SELECT node, affiliation FROM entities | 297 d = self.dbpool.runQuery(*withPEP("""SELECT node, affiliation FROM entities |
309 NATURAL JOIN affiliations | 298 NATURAL JOIN affiliations |
310 NATURAL JOIN nodes | 299 NATURAL JOIN nodes |
345 | 334 |
346 def __init__(self, nodeDbId, nodeIdentifier, config): | 335 def __init__(self, nodeDbId, nodeIdentifier, config): |
347 self.nodeDbId = nodeDbId | 336 self.nodeDbId = nodeDbId |
348 self.nodeIdentifier = nodeIdentifier | 337 self.nodeIdentifier = nodeIdentifier |
349 self._config = config | 338 self._config = config |
350 self.owner = None; | |
351 | 339 |
352 | 340 |
353 def _checkNodeExists(self, cursor): | 341 def _checkNodeExists(self, cursor): |
354 cursor.execute("""SELECT 1 as exist FROM nodes WHERE node_id=%s""", | 342 cursor.execute("""SELECT 1 as exist FROM nodes WHERE node_id=%s""", |
355 (self.nodeDbId,)) | 343 (self.nodeDbId,)) |
358 | 346 |
359 | 347 |
360 def getType(self): | 348 def getType(self): |
361 return self.nodeType | 349 return self.nodeType |
362 | 350 |
363 def getNodeOwner(self): | 351 def getOwners(self): |
364 if self.owner: | 352 d = self.dbpool.runQuery("""SELECT jid FROM nodes NATURAL JOIN affiliations NATURAL JOIN entities WHERE node_id=%s and affiliation='owner'""", (self.nodeDbId,)) |
365 return defer.succeed(self.owner) | 353 d.addCallback(lambda rows: [jid.JID(r[0]) for r in rows]) |
366 d = self.dbpool.runQuery("""SELECT jid FROM nodes NATURAL JOIN affiliations NATURAL JOIN entities WHERE node_id=%s""", (self.nodeDbId,)) | |
367 d.addCallback(lambda result: jid.JID(result[0][0])) | |
368 return d | 354 return d |
369 | 355 |
370 | 356 |
371 def getConfiguration(self): | 357 def getConfiguration(self): |
372 return self._config | 358 return self._config |
588 NATURAL JOIN entities | 574 NATURAL JOIN entities |
589 WHERE node_id=%s""", | 575 WHERE node_id=%s""", |
590 (self.nodeDbId,)) | 576 (self.nodeDbId,)) |
591 result = cursor.fetchall() | 577 result = cursor.fetchall() |
592 | 578 |
593 return [(jid.internJID(r[0]), r[1]) for r in result] | 579 return {jid.internJID(r[0]): r[1] for r in result} |
594 | 580 |
595 | 581 |
596 | 582 |
597 class LeafNode(Node): | 583 class LeafNode(Node): |
598 | 584 |
638 self.nodeDbId)) | 624 self.nodeDbId)) |
639 | 625 |
640 item_id = cursor.fetchone()[0]; | 626 item_id = cursor.fetchone()[0]; |
641 self._storeCategories(cursor, item_id, item_data.categories) | 627 self._storeCategories(cursor, item_id, item_data.categories) |
642 | 628 |
643 if access_model == const.VAL_AMODEL_ROSTER: | 629 if access_model == const.VAL_AMODEL_PUBLISHER_ROSTER: |
644 if const.OPT_ROSTER_GROUPS_ALLOWED in item_config: | 630 if const.OPT_ROSTER_GROUPS_ALLOWED in item_config: |
645 item_config.fields[const.OPT_ROSTER_GROUPS_ALLOWED].fieldType='list-multi' #XXX: needed to force list if there is only one value | 631 item_config.fields[const.OPT_ROSTER_GROUPS_ALLOWED].fieldType='list-multi' #XXX: needed to force list if there is only one value |
646 allowed_groups = item_config[const.OPT_ROSTER_GROUPS_ALLOWED] | 632 allowed_groups = item_config[const.OPT_ROSTER_GROUPS_ALLOWED] |
647 else: | 633 else: |
648 allowed_groups = [] | 634 allowed_groups = [] |
649 for group in allowed_groups: | 635 for group in allowed_groups: |
650 #TODO: check that group are actually in roster | 636 #TODO: check that group are actually in roster |
651 cursor.execute("""INSERT INTO item_groups_authorized (item_id, groupname) | 637 cursor.execute("""INSERT INTO item_groups_authorized (item_id, groupname) |
652 VALUES (%s,%s)""" , (item_id, group)) | 638 VALUES (%s,%s)""" , (item_id, group)) |
639 # TODO: whitelist access model | |
653 | 640 |
654 def _storeCategories(self, cursor, item_id, categories, update=False): | 641 def _storeCategories(self, cursor, item_id, categories, update=False): |
655 # TODO: handle canonical form | 642 # TODO: handle canonical form |
656 if update: | 643 if update: |
657 cursor.execute("""DELETE FROM item_categories | 644 cursor.execute("""DELETE FROM item_categories |
807 item = generic.stripNamespace(parseXml(data[0])) | 794 item = generic.stripNamespace(parseXml(data[0])) |
808 access_model = data[1] | 795 access_model = data[1] |
809 item_id = data[2] | 796 item_id = data[2] |
810 date = data[3] | 797 date = data[3] |
811 access_list = {} | 798 access_list = {} |
812 if access_model == const.VAL_AMODEL_ROSTER: #TODO: jid access_model | 799 if access_model == const.VAL_AMODEL_PUBLISHER_ROSTER: |
813 cursor.execute('SELECT groupname FROM item_groups_authorized WHERE item_id=%s', (item_id,)) | 800 cursor.execute('SELECT groupname FROM item_groups_authorized WHERE item_id=%s', (item_id,)) |
814 access_list[const.OPT_ROSTER_GROUPS_ALLOWED] = [r[0] for r in cursor.fetchall()] | 801 access_list[const.OPT_ROSTER_GROUPS_ALLOWED] = [r[0] for r in cursor.fetchall()] |
815 | 802 |
816 ret.append(container.ItemData(item, access_model, access_list, date=date)) | 803 ret.append(container.ItemData(item, access_model, access_list, date=date)) |
804 # TODO: whitelist item access model | |
817 return ret | 805 return ret |
818 | 806 |
819 items_data = [container.ItemData(generic.stripNamespace(parseXml(r[0])), r[1], r[2], date=r[3]) for r in result] | 807 items_data = [container.ItemData(generic.stripNamespace(parseXml(r[0])), r[1], r[2], date=r[3]) for r in result] |
820 return items_data | 808 return items_data |
821 | 809 |
822 def getItemsById(self, authorized_groups, unrestricted, itemIdentifiers): | 810 def getItemsById(self, authorized_groups, unrestricted, itemIdentifiers): |
823 """ Get items which are in the given list | 811 """Get items which are in the given list |
812 | |
824 @param authorized_groups: we want to get items that these groups can access | 813 @param authorized_groups: we want to get items that these groups can access |
825 @param unrestricted: if true, don't check permissions | 814 @param unrestricted: if true, don't check permissions |
826 @param itemIdentifiers: list of ids of the items we want to get | 815 @param itemIdentifiers: list of ids of the items we want to get |
827 @return: list of container.ItemData | 816 @return: list of container.ItemData |
828 ItemData.config will contains access_list (managed as a dictionnary with same key as for item_config) | 817 ItemData.config will contains access_list (managed as a dictionnary with same key as for item_config) |
847 item = generic.stripNamespace(parseXml(result[0])) | 836 item = generic.stripNamespace(parseXml(result[0])) |
848 access_model = result[1] | 837 access_model = result[1] |
849 item_id = result[2] | 838 item_id = result[2] |
850 date= result[3] | 839 date= result[3] |
851 access_list = {} | 840 access_list = {} |
852 if access_model == const.VAL_AMODEL_ROSTER: #TODO: jid access_model | 841 if access_model == const.VAL_AMODEL_PUBLISHER_ROSTER: |
853 cursor.execute('SELECT groupname FROM item_groups_authorized WHERE item_id=%s', (item_id,)) | 842 cursor.execute('SELECT groupname FROM item_groups_authorized WHERE item_id=%s', (item_id,)) |
854 access_list[const.OPT_ROSTER_GROUPS_ALLOWED] = [r[0] for r in cursor.fetchall()] | 843 access_list[const.OPT_ROSTER_GROUPS_ALLOWED] = [r[0] for r in cursor.fetchall()] |
844 #TODO: WHITELIST access_model | |
855 | 845 |
856 ret.append(container.ItemData(item, access_model, access_list, date=date)) | 846 ret.append(container.ItemData(item, access_model, access_list, date=date)) |
857 else: #we check permission before returning items | 847 else: #we check permission before returning items |
858 for itemIdentifier in itemIdentifiers: | 848 for itemIdentifier in itemIdentifiers: |
859 args = [self.nodeDbId, itemIdentifier] | 849 args = [self.nodeDbId, itemIdentifier] |