# HG changeset patch # User Goffi # Date 1503858819 -7200 # Node ID 20b82fb8de028c1a50a1e3eb027cf3bb768395ce # Parent d1f63ae1eaf49823b7787637e3915819196173c4 backend: check nodes/items permission on disco#items: - move node access check workflow from getItemsData to a new checkNodeAccess method - only accessible items are returned to an entity when doing a disco#items on a node - for PEP, nodes with presence access model are not returned if entity has not presence subscription from the node owner - all nodes are returned in normal pubsub service - new NotLeafNodeError exception when an action need to be done on Leaf node and it is not the case - /!\ access it not fully checked : items access models are not handled for items id in disco#items, and whitelist nodes are returned regardless if requestor is in the white list or not. Furthermore, publisher-roster access is not handled for nodes. diff -r d1f63ae1eaf4 -r 20b82fb8de02 sat_pubsub/backend.py --- a/sat_pubsub/backend.py Sun Aug 27 20:26:38 2017 +0200 +++ b/sat_pubsub/backend.py Sun Aug 27 20:33:39 2017 +0200 @@ -190,9 +190,21 @@ d.addCallback(lambda node: node.getType()) return d + def _getNodesIds(self, subscribed, pep, recipient): + # TODO: filter whitelist nodes + # TODO: handle publisher-roster (should probably be renamed to owner-roster for nodes) + if not subscribed: + allowed_accesses = {'open', 'whitelist'} + else: + allowed_accesses = {'open', 'presence', 'whitelist'} + return self.storage.getNodeIds(pep, recipient, allowed_accesses) - def getNodes(self, pep): - return self.storage.getNodeIds(pep) + def getNodes(self, requestor, pep, recipient): + if pep: + d = self.privilege.isSubscribedFrom(requestor, recipient) + d.addCallback(self._getNodesIds, pep, recipient) + return d + return self.storage.getNodeIds(pep, recipient) def getNodeMetaData(self, nodeIdentifier, pep, recipient=None): @@ -661,15 +673,75 @@ d.addErrback(self.unwrapFirstError) return d + @defer.inlineCallbacks + def checkNodeAccess(self, node, requestor): + """check if a requestor can access data of a node - def getItemsIds(self, nodeIdentifier, authorized_groups, unrestricted, maxItems=None, ext_data=None, pep=False, recipient=None): - d = self.storage.getNode(nodeIdentifier, pep, recipient) - d.addCallback(lambda node: node.getItemsIds(authorized_groups, - unrestricted, - maxItems, - ext_data)) - return d + @param node(Node): node to check + @param requestor(jid.JID): entity who want to access node + @return (tuple): permissions data with: + - owner(bool): True if requestor is owner of the node + - roster(None, ): roster of the requestor + None if not needed/available + - access_model(str): access model of the node + @raise error.Forbidden: access is not granted + @raise error.NotLeafNodeError: this node is not a leaf + """ + node, affiliation = yield _getAffiliation(node, requestor) + + if not iidavoll.ILeafNode.providedBy(node): + raise error.NotLeafNodeError() + + if affiliation == 'outcast': + raise error.Forbidden() + + # node access check + owner = affiliation == 'owner' + access_model = node.getAccessModel() + roster = None + + if access_model == const.VAL_AMODEL_OPEN or owner: + pass + elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER: + # FIXME: publisher roster should be used, not owner + roster = yield self.getOwnerRoster(node) + + if roster is None: + raise error.Forbidden() + if requestor not in roster: + raise error.Forbidden() + + authorized_groups = yield node.getAuthorizedGroups() + + if not roster[requestor].groups.intersection(authorized_groups): + # requestor is in roster but not in one of the allowed groups + raise error.Forbidden() + elif access_model == const.VAL_AMODEL_WHITELIST: + affiliations = yield node.getAffiliations() + try: + affiliation = affiliations[requestor.userhostJID()] + except KeyError: + raise error.Forbidden() + else: + if affiliation not in ('owner', 'publisher', 'member'): + raise error.Forbidden() + else: + raise Exception(u"Unknown access_model") + + defer.returnValue((affiliation, owner, roster, access_model)) + + @defer.inlineCallbacks + def getItemsIds(self, nodeIdentifier, requestor, authorized_groups, unrestricted, maxItems=None, ext_data=None, pep=False, recipient=None): + # FIXME: items access model are not checked + # TODO: check items access model + node = yield self.storage.getNode(nodeIdentifier, pep, recipient) + affiliation, owner, roster, access_model = yield self.checkNodeAccess(node, requestor) + ids = yield node.getItemsIds(authorized_groups, + unrestricted, + maxItems, + ext_data) + defer.returnValue(ids) def getItems(self, nodeIdentifier, requestor, recipient, maxItems=None, itemIdentifiers=None, ext_data=None): @@ -679,6 +751,7 @@ @defer.inlineCallbacks def getOwnerRoster(self, node, owners=None): + # FIXME: roster of publisher, not owner, must be used if owners is None: owners = yield node.getOwners() @@ -708,48 +781,11 @@ if ext_data is None: ext_data = {} node = yield self.storage.getNode(nodeIdentifier, ext_data.get('pep', False), recipient) - node, affiliation = yield _getAffiliation(node, requestor) - - if not iidavoll.ILeafNode.providedBy(node): + try: + affiliation, owner, roster, access_model = yield self.checkNodeAccess(node, requestor) + except error.NotLeafNodeError: defer.returnValue([]) - if affiliation == 'outcast': - raise error.Forbidden() - - - # node access check - owner = affiliation == 'owner' - access_model = node.getAccessModel() - roster = None - - if access_model == const.VAL_AMODEL_OPEN or owner: - pass - elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER: - roster = yield self.getOwnerRoster(node) - - if roster is None: - raise error.Forbidden() - - if requestor not in roster: - raise error.Forbidden() - - authorized_groups = yield node.getAuthorizedGroups() - - if not roster[requestor].groups.intersection(authorized_groups): - # requestor is in roster but not in one of the allowed groups - raise error.Forbidden() - elif access_model == const.VAL_AMODEL_WHITELIST: - affiliations = yield node.getAffiliations() - try: - affiliation = affiliations[requestor.userhostJID()] - except KeyError: - raise error.Forbidden() - else: - if affiliation not in ('owner', 'publisher', 'member'): - raise error.Forbidden() - else: - raise Exception(u"Unknown access_model") - # at this point node access is checked if owner: @@ -757,6 +793,7 @@ requestor_groups = None else: if roster is None: + # FIXME: publisher roster should be used, not owner roster = yield self.getOwnerRoster(node) if roster is None: roster = {} @@ -1215,6 +1252,7 @@ allowed_items.append(item) elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER: if owner_roster is None: + # FIXME: publisher roster should be used, not owner owner_roster= yield self.getOwnerRoster(node, owners) if owner_roster is None: owner_roster = {} @@ -1286,6 +1324,12 @@ return d def getNodes(self, requestor, service, nodeIdentifier): + """return nodes for disco#items + + Pubsub/PEP nodes will be returned if disco node is not specified + else Pubsub/PEP items will be returned + (according to what requestor can access) + """ try: pep = service.pep except AttributeError: @@ -1296,15 +1340,20 @@ if nodeIdentifier: d = self.backend.getItemsIds(nodeIdentifier, + requestor, [], requestor.userhostJID() == service, None, None, pep, service) + # items must be set as name, not node + d.addCallback(lambda items: [(None, item) for item in items]) else: - d = self.backend.getNodes(pep) + d = self.backend.getNodes(requestor.userhostJID(), + pep, + service) return d.addErrback(self._mapErrors) diff -r d1f63ae1eaf4 -r 20b82fb8de02 sat_pubsub/error.py --- a/sat_pubsub/error.py Sun Aug 27 20:26:38 2017 +0200 +++ b/sat_pubsub/error.py Sun Aug 27 20:33:39 2017 +0200 @@ -61,6 +61,7 @@ def __str__(self): return self.msg + class Deprecated(Exception): pass @@ -69,93 +70,79 @@ pass - class NodeExists(Error): pass - class NotSubscribed(Error): """ Entity is not subscribed to this node. """ - class SubscriptionExists(Error): """ There already exists a subscription to this node. """ +def NotLeafNodeError(Error): + """a leaf node is expected but we have a collection""" + class Forbidden(Error): pass - class NotAuthorized(Error): pass - class NotInRoster(Error): pass - class ItemNotFound(Error): pass - class ItemForbidden(Error): pass - class ItemRequired(Error): pass - class NoInstantNodes(Error): pass - class InvalidConfigurationOption(Error): msg = 'Invalid configuration option' - class InvalidConfigurationValue(Error): msg = 'Bad configuration value' - class NodeNotPersistent(Error): pass - class NoRootNode(Error): pass - class NoCallbacks(Error): """ There are no callbacks for this node. """ - - class NoCollections(Error): pass - class NoPublishing(Error): """ This node does not support publishing. diff -r d1f63ae1eaf4 -r 20b82fb8de02 sat_pubsub/pgsql_storage.py --- a/sat_pubsub/pgsql_storage.py Sun Aug 27 20:26:38 2017 +0200 +++ b/sat_pubsub/pgsql_storage.py Sun Aug 27 20:33:39 2017 +0200 @@ -205,9 +205,26 @@ row = cursor.fetchone() return self._buildNode(row) - def getNodeIds(self, pep): - d = self.dbpool.runQuery("""SELECT node from nodes WHERE pep is {}NULL""" - .format("NOT " if pep else "")) + def getNodeIds(self, pep, recipient, allowed_accesses=None): + """retrieve ids of existing nodes + + @param allowed_accesses(None, set): only nodes with access + in this set will be returned + None to return all nodes + @return (list[unicode]): ids of nodes + """ + if not pep: + query = "SELECT node from nodes WHERE pep is NULL" + values = [] + else: + query = "SELECT node from nodes WHERE pep=%s" + values = [recipient.userhost()] + + if allowed_accesses is not None: + query += "AND access_model IN %s" + values.append(tuple(allowed_accesses)) + + d = self.dbpool.runQuery(query, values) d.addCallback(lambda results: [r[0] for r in results]) return d