changeset 349:20b82fb8de02

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.
author Goffi <goffi@goffi.org>
date Sun, 27 Aug 2017 20:33:39 +0200
parents d1f63ae1eaf4
children 4d4575911060
files sat_pubsub/backend.py sat_pubsub/error.py sat_pubsub/pgsql_storage.py
diffstat 3 files changed, 123 insertions(+), 70 deletions(-) [+]
line wrap: on
line diff
--- 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)
 
 
--- 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.
--- 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